-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(webhooks): add example webhook receiver with docs #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- create minimal Node.js webhook receiver in examples/webhook-receiver - implement HMAC-SHA256 signature verification - handle panic.ui, panic.run, and status.set events (active) - include handlers for planned events (ingest, emotion, tell) - add comprehensive README with setup, security, and usage - add integration test script for end-to-end verification - update main README with webhooks section and example link - document webhook contract: signature format, event types, security 🌬 whisper: "one wire, one clear signal — let the field breathe outward." Co-authored-by: razvantirboaca <211984+razvantirboaca@users.noreply.github.com>
- add QUICKREF.md for at-a-glance webhook setup - includes start commands, configuration, event types - lists integration ideas and security reminders 🌬 whisper: "quick start, clear path — no friction to flow." Co-authored-by: razvantirboaca <211984+razvantirboaca@users.noreply.github.com>
|
Build preview ✅ Docker image (not pushed on PRs): ghcr.io/gratiaos/m3/m3-memory-server:sha-bb527a3 To test locally after checkout: # Build
docker build -t ghcr.io/gratiaos/m3/m3-memory-server:sha-bb527a3 -f server/Dockerfile .
# Run (example)
docker run --rm -p 3033:3033 \
-e M3_BIND=0.0.0.0:3033 \
-e M3_WEBHOOK_URL=$M3_WEBHOOK_URL \
-e M3_WEBHOOK_SECRET=$M3_WEBHOOK_SECRET \
ghcr.io/gratiaos/m3/m3-memory-server:sha-bb527a3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Constant-time comparison | ||
| return receivedSig === expectedSig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use timing-safe comparison for webhook signatures
Signature verification currently returns receivedSig === expectedSig even though the comment calls for a constant-time check. With M3_WEBHOOK_SECRET set, this string equality short-circuits on the first differing byte, exposing the receiver to timing attacks when validating X-M3-Signature; Node’s crypto.timingSafeEqual (after normalizing to Buffers) is needed here to avoid leaking verification timing.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements a minimal webhook receiver example for M3's Phase 3 (v0.1.9) goal of establishing a stable webhook contract with sample receivers. The implementation demonstrates HMAC-SHA256 signature verification and event handling patterns.
Key Changes:
- Added complete Node.js webhook receiver with zero external dependencies, handling panic, status, and planned event types
- Comprehensive documentation including setup guides, security best practices, and integration examples
- Integration test script for end-to-end verification of webhook flow
Reviewed Changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
examples/webhook-receiver/server.js |
Core webhook receiver implementing signature verification and event handlers |
examples/webhook-receiver/README.md |
Complete documentation with setup, security guidance, and integration ideas |
examples/webhook-receiver/QUICKREF.md |
Quick reference guide for common operations |
examples/webhook-receiver/test-integration.sh |
End-to-end integration test script |
examples/webhook-receiver/package.json |
Node.js project configuration with ESM support |
examples/webhook-receiver/.env.example |
Environment variable template |
examples/webhook-receiver/.gitignore |
Excludes node_modules, logs, and .env files |
README.md |
Added webhooks section with event types table and links to example receiver |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| req.on('data', chunk => body += chunk.toString()); | ||
|
|
||
| req.on('end', () => { |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing request body size limit. Without a limit, an attacker could send extremely large payloads causing memory exhaustion (DoS). Add a size check:
let body = '';
const maxSize = 1048576; // 1MB
let size = 0;
req.on('data', chunk => {
size += chunk.length;
if (size > maxSize) {
req.destroy();
res.writeHead(413, { 'Content-Type': 'text/plain' });
res.end('Payload Too Large');
return;
}
body += chunk.toString();
});| req.on('data', chunk => body += chunk.toString()); | |
| req.on('end', () => { | |
| const maxSize = 1048576; // 1MB | |
| let size = 0; | |
| let tooLarge = false; | |
| req.on('data', chunk => { | |
| size += chunk.length; | |
| if (size > maxSize) { | |
| tooLarge = true; | |
| req.destroy(); | |
| res.writeHead(413, { 'Content-Type': 'text/plain' }); | |
| res.end('Payload Too Large'); | |
| return; | |
| } | |
| body += chunk.toString(); | |
| }); | |
| req.on('end', () => { | |
| if (tooLarge) { | |
| // Already responded with 413 | |
| return; | |
| } |
| hmac.update(payload); | ||
| const expectedSig = hmac.digest('hex'); | ||
|
|
||
| return receivedSig === expectedSig; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documented signature verification example doesn't use constant-time comparison. To be consistent with the security best practice recommendation made for the main server code, update this example to use timingSafeEqual():
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySignature(signature, body, secret) {
// Parse: m3=t=1234567890,v1=abc123...
const parts = signature.slice(3).split(',');
const timestamp = parts[0]?.split('=')[1];
const receivedSig = parts[1]?.split('=')[1];
// Compute expected signature
const payload = `${timestamp}.${body}`;
const hmac = createHmac('sha256', secret);
hmac.update(payload);
const expectedSig = hmac.digest('hex');
// Constant-time comparison
const receivedBuf = Buffer.from(receivedSig, 'hex');
const expectedBuf = Buffer.from(expectedSig, 'hex');
return receivedBuf.length === expectedBuf.length &&
timingSafeEqual(receivedBuf, expectedBuf);
}| pkill -f "m3-memory-server" 2>/dev/null || true | ||
| pkill -f "webhook-receiver" 2>/dev/null || true |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pkill commands could unintentionally kill processes with similar names. Use more specific process identifiers or PID files for safer cleanup:
cleanup() {
echo ""
echo "🧹 Cleaning up..."
if [ -n "$RECEIVER_PID" ] && kill -0 "$RECEIVER_PID" 2>/dev/null; then
kill "$RECEIVER_PID" 2>/dev/null || true
fi
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
kill "$SERVER_PID" 2>/dev/null || true
fi
rm -f /tmp/m3-test-*.log
}This uses the captured PIDs instead of pattern matching, which is more reliable and won't affect other processes.
| pkill -f "m3-memory-server" 2>/dev/null || true | |
| pkill -f "webhook-receiver" 2>/dev/null || true | |
| if [ -n "$RECEIVER_PID" ] && kill -0 "$RECEIVER_PID" 2>/dev/null; then | |
| kill "$RECEIVER_PID" 2>/dev/null || true | |
| fi | |
| if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then | |
| kill "$SERVER_PID" 2>/dev/null || true | |
| fi |
| echo "" | ||
| echo "4️⃣ Webhook receiver log:" | ||
| echo " ─────────────────────────────" | ||
| tail -20 /tmp/m3-test-receiver.log | grep -A10 "Event:" |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The grep command might fail if no events are found, causing the script to exit due to set -e. Use grep with || true to prevent premature script termination:
tail -20 /tmp/m3-test-receiver.log | grep -A10 "Event:" || echo " (no events captured)"This ensures the script completes successfully even if no webhook events were logged.
| tail -20 /tmp/m3-test-receiver.log | grep -A10 "Event:" | |
| tail -20 /tmp/m3-test-receiver.log | grep -A10 "Event:" || echo " (no events captured)" |
| VALUE_FORCE_MOCK_UNTIL= # optional guard timestamp (RFC3339) | ||
| ``` | ||
|
|
||
| > |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Extra blank line before the note about M3_BEARER. This creates unnecessary whitespace. Remove the empty blockquote line:
>
> If `M3_BEARER` is set, all **write** endpoints require `Authorization: Bearer <token>`.Should be:
> If `M3_BEARER` is set, all **write** endpoints require `Authorization: Bearer <token>`.| > |
| const expectedSig = hmac.digest('hex'); | ||
|
|
||
| // Constant-time comparison | ||
| return receivedSig === expectedSig; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comparison receivedSig === expectedSig is not constant-time secure. JavaScript's === operator uses short-circuit evaluation and can leak timing information, making it vulnerable to timing attacks. Use crypto.timingSafeEqual() for constant-time comparison:
// Convert hex strings to buffers for constant-time comparison
const receivedBuf = Buffer.from(receivedSig, 'hex');
const expectedBuf = Buffer.from(expectedSig, 'hex');
// Constant-time comparison
try {
return receivedBuf.length === expectedBuf.length &&
crypto.timingSafeEqual(receivedBuf, expectedBuf);
} catch {
return false;
}| if (!timestamp || !receivedSig) { | ||
| return false; | ||
| } | ||
|
|
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing timestamp validation. The webhook should verify that the timestamp is recent to prevent replay attacks. Consider adding:
// After parsing timestamp
const now = Math.floor(Date.now() / 1000);
const maxAge = 300; // 5 minutes
if (Math.abs(now - parseInt(timestamp)) > maxAge) {
return false;
}This ensures that old webhook payloads cannot be replayed.
| // Validate timestamp to prevent replay attacks | |
| const now = Math.floor(Date.now() / 1000); | |
| const maxAge = 300; // 5 minutes | |
| if (Math.abs(now - parseInt(timestamp, 10)) > maxAge) { | |
| return false; | |
| } |
|
Build preview ✅ Docker image (not pushed on PRs): ghcr.io/gratiaos/m3/m3-memory-server:sha-bb527a3 To test locally after checkout: # Build
docker build -t ghcr.io/gratiaos/m3/m3-memory-server:sha-bb527a3 -f server/Dockerfile .
# Run (example)
docker run --rm -p 3033:3033 \
-e M3_BIND=0.0.0.0:3033 \
-e M3_WEBHOOK_URL=$M3_WEBHOOK_URL \
-e M3_WEBHOOK_SECRET=$M3_WEBHOOK_SECRET \
ghcr.io/gratiaos/m3/m3-memory-server:sha-bb527a3 |
Pull Request
Summary
Implements a minimal Node.js webhook receiver demonstrating M3's webhook integration pattern with HMAC-SHA256 signature verification. Addresses Phase 3 (v0.1.9) goal: "Stable webhook contract + sample receivers."
Linked Issues
Refs #1
Changes
New files:
examples/webhook-receiver/server.js— 145-line receiver, zero dependencies (Node http + crypto)examples/webhook-receiver/README.md— Setup, security, event types, integration ideasexamples/webhook-receiver/QUICKREF.md— At-a-glance referenceexamples/webhook-receiver/test-integration.sh— End-to-end verification scriptexamples/webhook-receiver/package.json,.env.example,.gitignoreUpdated:
README.md— Added webhooks section with event types table, security detailsEvent handlers:
panic.ui,panic.run,status.setingest,emotion,tell(handlers ready for when implemented)Security:
Screenshots / Demos (optional)
Tests
Manual Verification
cd examples/webhook-receiver && M3_WEBHOOK_SECRET=test npm startcurl http://localhost:4001/health→{"status":"ok"}M3_WEBHOOK_URL=http://localhost:4001/webhook M3_WEBHOOK_SECRET=test cargo runcurl -X POST localhost:3033/panic -H "Content-Type: application/json" -d '{}'Checklist
type(scope): summary)CHANGELOG.mdif docs or user-facing behavior changeddocs/marks/or Human Log if relevant🌬 whisper: "one wire, one clear signal — let the field breathe outward."
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.