feat(gateway): Complete implementation + fix Supermemory adapter#700
Conversation
- Add Gateway router class (processWebhook, deliver, registerAdapter)
- Add rules engine (matchesRule, findMatchingRules, evaluateCondition)
- Add Slack adapter (v0 HMAC, app_mention/message parsing, chat.postMessage)
- Add WhatsApp adapter (X-Hub-Signature-256, Cloud API)
- Add Telegram adapter (secret token, Bot API)
- Fix @agent-relay/memory Supermemory adapter filter format
(was: { agentId: 'x' }, now: { AND: [{ key: 'agentId', value: 'x' }] })
This is the shared gateway primitive for multi-platform conversational agents.
Open source — consumed by Sage, NightCTO, MSD.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| return text | ||
| .replace(/<@[A-Z0-9]+\|([^>]+)>/g, '@$1') |
| case '!=': | ||
| return messageValue !== compareValue; |
There was a problem hiding this comment.
🔴 != operator with null is not the logical negation of ==, causing contradictory rule matching
The == operator has special handling for null (treating undefined as equal to null at rules-engine.ts:70-71), but the != operator at line 75 uses plain messageValue !== compareValue with no corresponding special handling. When a field is missing (messageValue is undefined) and the condition compares against null:
$.field == null→true(via the special null handling)$.field != null→undefined !== null→ alsotrue
Both a condition and its negation return true for the same message, which means rules using $.field != null to filter for messages where a field exists will incorrectly match messages where the field is absent.
| case '!=': | |
| return messageValue !== compareValue; | |
| case '!=': | |
| if (compareValue === null) { | |
| return messageValue !== null && messageValue !== undefined; | |
| } | |
| return messageValue !== compareValue; |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
✅ Resolved: The current code at rules-engine.ts:78-82 already contains the fix: the != operator now has special null handling (checking messageValue !== null && messageValue !== undefined), which is exactly what the suggested edit proposed. This bug has been resolved.
- Regenerate package-lock.json so @agent-relay/gateway is present, unblocking every `npm ci` step (lint, tests, install, e2e, coverage, codeql, licenses, npm audit, standalone smoke). - Bump gateway to 4.0.5 and its @agent-relay/sdk dependency to 4.0.5 to match the rest of the monorepo after the v4.0.5 release. - Add `gateway` to the `publish-packages` matrix in publish.yml so the new package is published alongside the other workspaces on release. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The == operator treats undefined as equal to null (rules-engine.ts:70-71), but != was a plain strict-inequality so `$.field == null` and `$.field != null` both returned true when the field was absent — a condition and its negation matching the same message. Mirror the null-handling on !=: when comparing against null, return true only if messageValue is neither null nor undefined. Addresses Devin review comment on PR #700. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CodeQL js/polynomial-redos flagged the condition regex at rules-engine.ts:36.
Two unbounded \s* groups flanking an operator alternation caused O(n²)
backtracking on whitespace-heavy input like `\$.a\t\t...>` when the operator
failed to match.
Bound both whitespace regions to \s{0,16}. 16 chars of whitespace around an
operator is already pathological input — no legitimate rule uses that — and
the bound eliminates the polynomial backtrack while preserving ergonomics
for real rules.
A full parser rewrite (hand-rolled prefix-match, faster hot path, trivially
extensible) is tracked as a follow-up. This change is the minimum-scope fix
to clear the CodeQL finding in this PR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| verify(_payload: string, headers: HeaderMap): boolean { | ||
| const token = headers['x-telegram-bot-api-secret-token'] as string | undefined; | ||
| if (!token) return false; | ||
| return token === this.webhookSecretToken; |
There was a problem hiding this comment.
🟡 Telegram verify uses timing-unsafe === comparison for secret token
The Telegram adapter's verify method at line 44 uses a plain === string comparison for the webhook secret token, which is vulnerable to timing side-channel attacks. An attacker who can send requests to the webhook endpoint could potentially deduce the secret token character-by-character by measuring response time differences. Both the Slack adapter (packages/gateway/src/adapters/slack.ts:103) and the WhatsApp adapter (packages/gateway/src/adapters/whatsapp.ts:57) correctly use crypto.timingSafeEqual for their secret comparisons. The Telegram adapter doesn't even import crypto.
Prompt for agents
The Telegram adapter's verify method uses a plain === string comparison for the webhook secret token at line 44. This should use crypto.timingSafeEqual to be consistent with the Slack and WhatsApp adapters and to prevent timing side-channel attacks.
To fix:
1. Add import crypto from 'crypto' at the top of packages/gateway/src/adapters/telegram.ts (alongside the existing type imports)
2. Replace the === comparison with crypto.timingSafeEqual wrapped in try-catch (same pattern used in slack.ts:102-106 and whatsapp.ts:56-60):
try { return crypto.timingSafeEqual(Buffer.from(token), Buffer.from(this.webhookSecretToken)); } catch { return false; }
The try-catch handles the case where the buffers have different lengths (timingSafeEqual throws in that case).
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
@agent-relay/gateway— router, rules engine, Slack/WhatsApp/Telegram adapters@agent-relay/memorySupermemory filter format (was sending flat filters, now uses{ AND: [{ key, value }] })Gateway (
packages/gateway/)Types were already defined (172 lines). This PR adds the implementation:
router.ts— Gateway class: verify → parse → match rules → dispatchrules-engine.ts— rule matching with priority sortingadapters/slack.ts— Slack v0 HMAC, event parsing, Web API deliveryadapters/whatsapp.ts— Meta Cloud API, X-Hub-Signature-256adapters/telegram.ts— Bot API, secret token headerMemory Fix (
packages/memory/)Supermemory adapter was sending:
{ "filters": { "agentId": "sage" } }Now sends the correct format:
{ "filters": { "AND": [{ "key": "agentId", "value": "sage" }] } }Consumers
Test plan
npm run buildpasses for gateway + memory packages🤖 Generated with Claude Code