Skip to content

broker: add Slack message broker — Phase 1 (Cloudflare Worker)#65

Merged
benvinegar merged 3 commits intomainfrom
feat/slack-broker
Feb 19, 2026
Merged

broker: add Slack message broker — Phase 1 (Cloudflare Worker)#65
benvinegar merged 3 commits intomainfrom
feat/slack-broker

Conversation

@baudbot-agent
Copy link
Collaborator

Summary

Self-contained Cloudflare Worker (slack-broker/) that routes messages between Slack workspaces and individual baudbot servers with end-to-end encryption. Deployable via cd slack-broker && wrangler deploy.

Implements Phase 1 from docs/slack-broker-spec.md: core broker skeleton, crypto layer, OAuth install flow, inbound/outbound paths, server registration, and tests.

What's Included

Crypto Layer (src/crypto/)

  • seal.tscrypto_box_seal sealed boxes for inbound (Slack→server) encryption. Broker encrypts with server pubkey, CANNOT decrypt.
  • box.tscrypto_box authenticated encryption for outbound (server→Slack). Broker decrypts transiently to post to Slack, then zeroes plaintext.
  • verify.ts — Ed25519 signatures for envelope authentication. Deterministic canonicalization prevents field-ordering attacks.

Slack Integration (src/slack/)

  • events.ts — Slack Events API handler with HMAC-SHA256 signature verification and 5-minute replay protection
  • oauth.ts — OAuth install flow (redirect → callback → store bot token → return auth code)
  • api.ts — Slack Web API helpers: chat.postMessage, reactions.add, chat.update

Routing (src/routing/)

  • registry.ts — KV-backed workspace registry (workspace_id → {server_url, server_pubkey, bot_token, status})
  • forward.ts — Encrypt event with sealed box → POST to server callback URL (10s timeout, no queuing)

API Endpoints (src/api/)

  • register.ts — Server registration with auth code verification + HTTPS-only callback URLs; unregister with signature verification
  • send.ts — Outbound: receive encrypted reply → verify server signature → decrypt → post to Slack → zero plaintext

Router (src/index.ts)

Routes all requests, derives broker keypairs from a single 32-byte seed secret.

Method Path Description
GET /health Health check
GET /api/broker-pubkey Public key distribution
POST /slack/events Slack Events API webhook
GET /slack/oauth/install Start OAuth install
GET /slack/oauth/callback Handle OAuth callback
POST /api/register Register server
DELETE /api/register Unlink server
POST /api/send Outbound encrypted message

Security Properties

  • ✅ Broker CANNOT decrypt inbound messages (sealed box — no broker private key involved)
  • ✅ Outbound message body encrypted in transit (authenticated, zeroed after Slack API call)
  • ✅ No message storage or persistence
  • ✅ Replay protection (5-minute timestamp windows on all paths)
  • ✅ Auth code hashed with SHA-256 before storage
  • ✅ Constant-time signature comparison
  • ✅ HTTPS-only callback URLs enforced

Tests

54 tests across 3 suites, all passing:

  • crypto.test.ts (24 tests) — sealed box encrypt/decrypt, authenticated box, signatures, encoding, edge cases
  • routing.test.ts (16 tests) — KV registry CRUD, auth code hashing, event forwarding with mocked fetch
  • integration.test.ts (14 tests) — end-to-end inbound/outbound flows, Slack signature verification, registration cycle, replay protection

Docs Updated

  • AGENTS.md / CLAUDE.md — added slack-broker/ to repo layout
  • docs/architecture.md — added slack-broker to source tree diagram

Dependencies

  • tweetnacl — libsodium-compatible crypto (lightweight, no native deps, Workers-compatible)
  • tweetnacl-util — encoding utilities
  • @cloudflare/workers-types — TypeScript types (dev)
  • vitest — test runner (dev)
  • wrangler — Cloudflare Workers CLI (dev)

@socket-security
Copy link

socket-security bot commented Feb 19, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedtweetnacl@​1.0.310010010075100
Addedvitest@​2.1.9981007999100
Addedlibsodium-wrappers-sumo@​0.8.21001009989100
Addedwrangler@​3.114.17981009496100
Added@​cloudflare/​workers-types@​4.20260219.0100100100100100

View full report

@socket-security
Copy link

socket-security bot commented Feb 19, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm vite is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: slack-broker/package-lock.jsonnpm/vitest@2.1.9npm/vite@5.4.21

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/vite@5.4.21. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@greptile-apps
Copy link

greptile-apps bot commented Feb 19, 2026

Greptile Summary

This PR implements Phase 1 of the Slack broker — a self-contained Cloudflare Worker that routes messages between Slack workspaces and individual baudbot servers with end-to-end encryption.

Key Changes:

  • Crypto layer implementing sealed boxes (inbound), authenticated encryption (outbound), and Ed25519 signatures
  • Complete OAuth install flow with auth code verification for server registration
  • Slack Events API handler with HMAC-SHA256 signature verification and replay protection
  • KV-backed workspace registry with SHA-256 auth code hashing
  • Event forwarding with 10-second timeout (fire-and-forget, no queuing)
  • Outbound message handling that decrypts transiently and zeros plaintext after posting to Slack
  • 54 passing tests across crypto, routing, and integration suites
  • Documentation updated in AGENTS.md and docs/architecture.md

Security Highlights:

  • Broker cannot decrypt inbound messages (sealed box encryption)
  • Outbound plaintext zeroed immediately after Slack API call
  • Constant-time signature comparison prevents timing attacks
  • HTTPS-only callback URLs enforced
  • 5-minute replay protection on all message paths

Minor Issue:

  • tweetnacl-util dependency is unused and can be removed

Confidence Score: 5/5

  • This PR is safe to merge with excellent security properties and comprehensive test coverage
  • The implementation demonstrates strong security practices (sealed box encryption, signature verification, replay protection, memory zeroing) with comprehensive tests (54 tests covering crypto primitives, routing, and end-to-end flows). The code is well-structured, thoroughly documented, and follows the project's conventions. The only issue is a minor unused dependency that doesn't affect functionality.
  • No files require special attention

Important Files Changed

Filename Overview
slack-broker/src/crypto/seal.ts Implements sealed box encryption for inbound messages; broker can encrypt but cannot decrypt
slack-broker/src/crypto/box.ts Implements authenticated encryption for outbound messages with memory zeroing
slack-broker/src/crypto/verify.ts Ed25519 signature creation/verification with deterministic canonicalization
slack-broker/src/slack/events.ts Slack Events API handler with HMAC signature verification and 5-minute replay protection
slack-broker/src/routing/forward.ts Encrypts and forwards Slack events to registered servers with 10s timeout
slack-broker/src/api/register.ts Server registration with auth code verification and HTTPS-only callback URLs
slack-broker/src/api/send.ts Outbound message handler: decrypts, posts to Slack, zeros plaintext
slack-broker/src/index.ts Worker entry point, derives keypairs from seed and routes requests
slack-broker/package.json Dependencies and scripts for Cloudflare Worker development

Sequence Diagram

sequenceDiagram
    participant User
    participant Slack
    participant Broker as Cloudflare Worker<br/>(Slack Broker)
    participant Server as Baudbot Server
    
    Note over User,Server: OAuth Installation Flow
    User->>Broker: GET /slack/oauth/install
    Broker->>Slack: Redirect to OAuth authorize
    Slack->>User: Authorization page
    User->>Slack: Approve app
    Slack->>Broker: GET /slack/oauth/callback?code=...
    Broker->>Slack: Exchange code for bot token
    Slack-->>Broker: bot_token + team_id
    Broker->>Broker: Generate auth_code, hash with SHA-256
    Broker-->>User: Display auth_code (for server setup)
    
    Note over User,Server: Server Registration
    Server->>Broker: POST /api/register<br/>(workspace_id, server_pubkey, auth_code)
    Broker->>Broker: Verify auth_code hash
    Broker->>Broker: Activate workspace (status: pending → active)
    Broker-->>Server: broker_pubkey, broker_signing_pubkey
    
    Note over Slack,Server: Inbound: Slack → Server (Sealed Box)
    Slack->>Broker: POST /slack/events<br/>(event payload, HMAC signature)
    Broker->>Broker: Verify HMAC-SHA256 signature
    Broker->>Broker: Encrypt with crypto_box_seal<br/>(server_pubkey, ephemeral keypair)
    Broker->>Broker: Sign envelope with Ed25519
    Broker->>Server: POST callback_url<br/>(encrypted event + signature)
    Server->>Server: Verify broker signature
    Server->>Server: Decrypt with crypto_box_seal_open<br/>(ONLY server can decrypt)
    
    Note over Server,Slack: Outbound: Server → Slack (Authenticated Box)
    Server->>Server: Encrypt body with crypto_box<br/>(broker_pubkey, server_secretkey)
    Server->>Server: Sign request with Ed25519
    Server->>Broker: POST /api/send<br/>(workspace_id, encrypted_body, signature)
    Broker->>Broker: Verify server signature
    Broker->>Broker: Decrypt body transiently
    Broker->>Slack: POST chat.postMessage<br/>(plaintext message)
    Broker->>Broker: Zero plaintext from memory
    Broker-->>Server: Success response
Loading

Last reviewed commit: ec1b3b1

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

23 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

},
"dependencies": {
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
Copy link

Choose a reason for hiding this comment

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

tweetnacl-util is listed as a dependency but not imported anywhere in the codebase

Suggested change
"tweetnacl-util": "^0.15.1"
"tweetnacl": "^1.0.3"
Prompt To Fix With AI
This is a comment left during a code review.
Path: slack-broker/package.json
Line: 15

Comment:
`tweetnacl-util` is listed as a dependency but not imported anywhere in the codebase

```suggestion
    "tweetnacl": "^1.0.3"
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Collaborator Author

@baudbot-agent baudbot-agent left a comment

Choose a reason for hiding this comment

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

Fresh-Eyes Code Review: slack-broker/

Reviewed the entire slack-broker/ Cloudflare Worker — crypto, API endpoints, Slack integration, routing, and test suite. All 54 tests pass. Overall this is solid, well-structured code with good security fundamentals. Below are my findings.


🔴 CRITICAL

1. Registration allows server hijacking — auth_code_hash not cleared after use

File: src/api/register.ts (lines 93-107) + src/routing/registry.ts

After successful registration, activateWorkspace() does NOT clear the auth_code_hash from the workspace record. This means:

  1. An attacker who obtains the auth code (e.g., user shares it insecurely, shoulder surfing, clipboard history) can re-register at any time — even after the legitimate server is active — replacing server_pubkey, server_signing_pubkey, and server_callback_url.
  2. The legitimate server is silently disconnected with no notification.
  3. All inbound events now flow to the attacker's server encrypted with the attacker's public key.

The auth code is a one-time credential that should be invalidated on first use.

Fix: Clear auth_code_hash (or set to empty string) after successful activation in activateWorkspace(). Also consider rejecting registration when the workspace is already active unless the current server explicitly unregisters first.

2. Re-OAuth overwrites active workspace — bot token and auth_code replaced

File: src/slack/oauth.ts (line 138) → src/routing/registry.ts createPendingWorkspace()

If someone completes the OAuth flow for a workspace that already has status: "active", createPendingWorkspace() unconditionally overwrites the entire record — including bot_token, auth_code_hash, and crucially resetting status to "pending". This:

  1. Breaks the active server's connection (events stop forwarding).
  2. Replaces the bot token, potentially with a token from a different Slack app install.
  3. Gives the new OAuth completer a fresh auth_code to register their own server.

Any Slack workspace admin (not just the one who originally installed) can trigger this.

Fix: Check if the workspace is already active in the OAuth callback. If so, either reject the re-install or require the current server to unregister first. At minimum, preserve the existing server linkage.

3. Sealed box nonce derivation is incompatible with libsodium

File: src/crypto/seal.ts (lines 28-34)

The nonce is derived using SHA-512(ephemeral_pk || recipient_pk) truncated to 24 bytes. The actual libsodium crypto_box_seal uses BLAKE2B(ephemeral_pk || recipient_pk) (specifically crypto_generichash with 24-byte output).

This means:

  • A baudbot server using actual libsodium (e.g., libsodium.js, Python's PyNaCl, Go's golang.org/x/crypto/nacl) to call crypto_box_seal_open will fail to decrypt messages from this broker.
  • The spec says "All encryption uses libsodium" but the implementation is not interoperable.

This isn't a security flaw per se (SHA-512 truncated to 24 bytes is a fine nonce derivation), but it's a correctness bug that will cause real-world failures when the server-side client is implemented.

Fix: Either:

  • (a) Use BLAKE2B for nonce derivation to match libsodium (requires adding a BLAKE2B implementation — not in tweetnacl), or
  • (b) Document prominently that this is a custom sealed-box variant, NOT libsodium-compatible crypto_box_seal, and ensure the server client uses the same SHA-512 derivation. Update doc comments and README claims accordingly.

I'd recommend (a) for interoperability. Consider using libsodium-wrappers-sumo which works in Workers and provides crypto_box_seal directly.


🟡 IMPORTANT

4. Canonicalization delimiter injection — pipe character not validated

Files: src/crypto/verify.ts (lines 63-67, 76-80)

The canonical signing format uses | as a delimiter:

"workspace_id|timestamp|encrypted_payload_base64"
"workspace_id|action|timestamp|encrypted_body_base64"

Neither workspace_id nor action are validated to exclude |. A malicious workspace_id like T123|chat.postMessage|0| could forge a canonical form that collides with a different legitimate request, potentially allowing signature reuse across different contexts.

Slack team IDs don't contain |, so this is more of a defense-in-depth issue — but the register endpoint doesn't validate workspace_id format.

Fix: Validate that workspace_id matches the Slack team ID format (/^T[A-Z0-9]+$/) at registration time. Or use a length-prefixed encoding instead of delimiters.

5. hashAuthCode uses unsalted SHA-256 — vulnerable to precomputation

File: src/routing/registry.ts (lines 116-122)

The auth code hash is SHA-256(authCode) with no salt. Auth codes are 64 hex chars (32 bytes of entropy), so rainbow tables aren't practical. However, this is a bad pattern:

  • If KV data is ever exposed (breach, admin access, Cloudflare incident), the unsalted hash allows offline brute-force.
  • More practically: it allows timing-based confirmation — an attacker who knows a candidate auth code can hash it and compare against the stored hash (if they gain KV read access).

The 256-bit entropy of the auth code makes this academic rather than exploitable, but using HMAC-SHA256 with a secret as the key (e.g., HMAC(SLACK_SIGNING_SECRET, authCode)) would be trivial and strictly better.

Fix: Use HMAC-SHA256 keyed with a broker secret instead of bare SHA-256.

6. Bot token stored in plaintext in KV

File: src/routing/registry.ts (lines 65, 80)

The comment says "Cloudflare KV is encrypted at rest" which is true, but this only protects against physical disk access at Cloudflare's data centers. The bot token is readable in plaintext by:

  • Anyone with Cloudflare dashboard access
  • wrangler kv:key get CLI
  • Any Worker code with the KV binding

The spec mentions "bot_token (encrypted at rest in KV)" but the implementation stores it as a plaintext JSON field. For the security model this broker advertises, the token should be encrypted with the broker's key before storage.

Fix: Encrypt the bot_token with nacl.secretbox using a key derived from BROKER_PRIVATE_KEY before storing in KV. Decrypt on read in getWorkspace().

7. forwardEvent does not validate server_url is HTTPS

File: src/routing/forward.ts

The register endpoint validates that server_callback_url uses HTTPS (in register.ts line 82), which is good. However, forwardEvent does NOT re-validate this. If the URL in KV is somehow modified (KV tampering, race condition during update), the broker would send the encrypted envelope over plain HTTP.

This is defense-in-depth since KV tampering would be a much larger issue, but a one-line check is cheap insurance.

Fix: Add if (!workspace.server_url.startsWith('https://')) return { ok: false, error: "invalid server URL" }; early in forwardEvent.

8. No rate limiting on any endpoint

Files: src/index.ts, src/api/register.ts, src/api/send.ts

There is zero rate limiting. The /api/register endpoint is particularly sensitive — an attacker can brute-force auth codes (64 hex chars makes this infeasible, but the pattern is concerning). The /api/send endpoint could be abused to spam Slack channels if a server's signing key is compromised.

The spec lists rate limiting as Phase 3, which is fine for MVP, but it should be noted as a pre-production requirement.

9. zeroBytes is ineffective in JavaScript

File: src/crypto/box.ts (lines 73-75)

arr.fill(0) does zero the Uint8Array contents, but JavaScript/V8's garbage collector may have already copied the data elsewhere in memory (e.g., during JSON.parse, string operations, V8 internal buffers). The decodeUTF8(decryptedBytes) on line 131 of send.ts creates a JavaScript string which is immutable and cannot be zeroed. The JSON.parse on line 136 creates more string copies.

This isn't fixable in JavaScript — it's a fundamental limitation. The zeroBytes call is still worth keeping as a best-effort measure, but the security claim "zeroes the plaintext from memory immediately after posting" is misleading. The plaintext string from JSON.parse will persist until GC collects it.

Fix: Update documentation and comments to say "best-effort memory cleanup" rather than implying deterministic zeroing. Consider noting this limitation in the README's security properties section.


🟢 MINOR

10. X25519 and Ed25519 derived from same seed

File: src/index.ts (lines 69-70)

Using the same 32-byte BROKER_PRIVATE_KEY as both an X25519 secret key and an Ed25519 seed is mathematically safe (the derived public keys will differ, and the algorithms operate on different curves), but it's an unusual pattern that makes key management more fragile. If the seed is compromised, both encryption and signing are compromised simultaneously.

Fix: Consider using two separate secrets, or at minimum document this design choice prominently.

11. Missing test coverage for several edge cases

Files: test/*.test.ts

Gaps I noticed:

  • No tests for handleSlackEvent (the main event handler function) — only verifySlackSignature is tested directly
  • No tests for handleSend or handleRegister (the HTTP handler functions) — only the underlying crypto/registry functions
  • No tests for OAuth flow (handleOAuthInstall, handleOAuthCallback)
  • No test that the url_verification challenge response works correctly
  • No negative test for sealedBoxDecrypt with wrong public key but correct private key (tests wrong keypair, but not mismatched pk/sk)
  • No test that boxEncryptboxDecrypt round-trip works across different process boundaries (simulating server→broker)

The tests are good unit tests for the crypto and registry layers, but there are no handler-level integration tests.

12. callSlackApi does not validate the method parameter

File: src/slack/api.ts (line 86)

The Slack API method name is interpolated directly into a URL: `https://slack.com/api/${method}`. While this is only called from executeSlackAction which validates the action, the function itself could be misused if called directly.

Fix: Validate method against an allowlist, or make the function non-exported (it's already module-private via callSlackApi not being exported, so this is fine as-is — disregard).

13. Missing Content-Type validation on POST endpoints

Files: src/api/register.ts, src/api/send.ts, src/slack/events.ts

POST endpoints call request.json() without checking Content-Type. Not a security issue since request.json() will throw on non-JSON regardless, but adding a Content-Type check produces clearer error messages.

14. OAuth redirect_uri derived from request.url

File: src/slack/oauth.ts (lines 58-59, 112)

The redirect_uri is derived from request.url, which in Workers comes from the incoming request's Host header. An attacker could manipulate the Host header to point the redirect to a different origin. In Cloudflare Workers with custom domains, the Host header is validated by Cloudflare, so this is safe in practice. But if deployed behind a proxy that allows host spoofing, it could be exploited.

Fix: Consider hardcoding the base URL or deriving it from a config variable rather than the request.


ℹ️ NOTES

15. Slack event retries not deduplicated

Slack retries events after 3 seconds if it doesn't get a 200 response in time. The broker ACKs immediately and forwards via waitUntil, so retries are unlikely. But if the Worker cold-starts slowly or KV is slow, Slack might retry. The server will receive duplicate encrypted events. This is expected behavior (spec says "no queuing") but worth documenting for server implementers.

16. setTimeout in Workers

src/routing/forward.ts uses setTimeout + AbortController for fetch timeout. This works in Cloudflare Workers, but AbortSignal.timeout(10_000) (available since compatibility_date 2024-12-30) would be cleaner and avoids the need for manual cleanup.

17. KV namespace interface re-declared

The KVNamespace interface is declared both in src/index.ts and src/routing/registry.ts. These should share a single type definition.

18. Spec deviation: /api/heartbeat and /api/react and /api/update not implemented

The spec lists POST /api/heartbeat, POST /api/react, and POST /api/update as separate endpoints. The implementation handles reactions.add and chat.update through /api/send with different action values — which is cleaner. Heartbeat is not implemented. These are likely Phase 2/3 items but should be called out.


Summary

Severity Count
🔴 CRITICAL 3
🟡 IMPORTANT 6
🟢 MINOR 5
ℹ️ NOTE 4

Must fix before merge: #1 (auth_code reuse), #2 (re-OAuth overwrites active workspace), #3 (sealed box incompatibility).

Strong recommendations: #4 (delimiter injection), #6 (bot token encryption), #9 (document zeroBytes limitations).

The crypto primitives are correctly used, the architecture matches the spec well, and the code quality is high. The main gaps are in the registration/lifecycle security rather than the cryptographic layer.

@baudbot-agent
Copy link
Collaborator Author

Security Review Fixes — All 9 Issues Addressed

🔴 Critical (3/3)

1. Auth code reuse allows server hijacking

  • activateWorkspace() now clears auth_code_hash after successful activation (one-time use)
  • handleRegister() rejects registration when workspace is already active (409 Conflict)
  • handleRegister() rejects registration when auth_code_hash is empty (already consumed)
  • Files: src/routing/registry.ts, src/api/register.ts

2. Re-OAuth overwrites active workspace

  • OAuth callback now checks if workspace is already active before creating a pending record
  • Returns a user-friendly HTML page explaining they must unregister first
  • File: src/slack/oauth.ts

3. Sealed box nonce uses SHA-512 instead of BLAKE2B

  • Replaced custom tweetnacl-based sealed box with libsodium-wrappers-sumo
  • Now uses native crypto_box_seal / crypto_box_seal_open with BLAKE2B nonce derivation
  • Fully interoperable with standard libsodium on the server side
  • Added libsodium-wrappers-sumo dependency (works in Cloudflare Workers)
  • File: src/crypto/seal.ts, package.json

🟡 Important (6/6)

4. Pipe delimiter injection in canonicalization

  • Registration now validates workspace_id matches /^T[A-Z0-9]+$/ (Slack team ID format)
  • Prevents injection of | characters into canonicalized signature strings
  • File: src/api/register.ts

5. hashAuthCode uses unsalted SHA-256

  • Switched to HMAC-SHA256 keyed with BROKER_PRIVATE_KEY
  • Prevents offline brute-force without knowledge of the broker's secret
  • Updated all callers: oauth.ts, register.ts
  • File: src/routing/registry.ts

6. Bot token stored in plaintext in KV

  • Added encryptBotToken() / decryptBotToken() using nacl.secretbox
  • Key derived from first 32 bytes of BROKER_PRIVATE_KEY
  • createPendingWorkspace() now encrypts before storage
  • handleSend() decrypts on read
  • Files: src/routing/registry.ts, src/api/send.ts, src/slack/oauth.ts

7. forwardEvent doesn't re-validate HTTPS

  • Added HTTPS protocol check at the start of forwardEvent()
  • Rejects http:// URLs with a clear error message
  • File: src/routing/forward.ts

8. Rate limiting noted as missing (Phase 3)

  • Added TODO comment in src/index.ts router noting rate limiting as a pre-production requirement
  • No code change needed

9. zeroBytes is misleading in docs

  • Updated zeroBytes() JSDoc to say "best-effort memory cleanup"
  • Added note that JS strings from JSON.parse / TextDecoder cannot be deterministically zeroed
  • Updated README security properties section
  • Files: src/crypto/box.ts, README.md

Tests

  • 54 → 64 tests (10 new) — all passing
  • New tests: auth code clearing, active workspace rejection, bot token encrypt/decrypt, workspace_id format validation, HTTPS enforcement, HMAC key differentiation

Files Changed

13 files, +298 / -85 lines

Baudbot added 3 commits February 18, 2026 23:42
Self-contained Cloudflare Worker at slack-broker/ that routes messages
between Slack workspaces and individual baudbot servers with end-to-end
encryption.

Crypto layer:
- crypto_box_seal (sealed boxes) for inbound Slack→server path
- crypto_box (authenticated encryption) for outbound server→Slack path
- Ed25519 signatures on all envelopes for authentication
- All primitives via tweetnacl (lightweight, no native deps, Workers-compatible)

Routing:
- KV-backed workspace registry (workspace_id → server config)
- Sealed box encrypt + forward to server callback URL
- 10s timeout, graceful error handling, no message queuing

Slack integration:
- Events API handler with HMAC-SHA256 signature verification
- 5-minute replay protection on all timestamps
- OAuth install flow with state parameter + auth code generation
- Slack Web API helpers (postMessage, reactions.add, chat.update)

API endpoints:
- POST /api/register — server registration with auth code verification
- DELETE /api/register — server unlink with signature verification
- POST /api/send — outbound encrypted message → decrypt → post to Slack
- GET /api/broker-pubkey — public key distribution

Security:
- Broker CANNOT decrypt inbound messages (sealed box)
- Outbound plaintext zeroed immediately after posting to Slack
- Auth codes hashed with SHA-256 before storage
- Callback URLs must use HTTPS
- Constant-time signature comparison

Tests: 54 tests across 3 suites (crypto, routing, integration)
Docs: AGENTS.md and architecture.md updated with slack-broker layout
Addresses review feedback — encoding utilities are implemented in
src/util/encoding.ts, tweetnacl-util was never imported.
…to, token encryption

Critical fixes:
1. Auth code reuse: clear auth_code_hash after activation, reject
   re-registration of already-active workspaces (409 Conflict)
2. Re-OAuth overwrites active workspace: check workspace status in
   OAuth callback, reject re-install when server is active
3. Sealed box nonce: switch from SHA-512 to libsodium-wrappers-sumo
   with native crypto_box_seal (BLAKE2B nonce, spec-compliant)

Important fixes:
4. Pipe delimiter injection: validate workspace_id matches /^T[A-Z0-9]+$/
5. Unsalted hash: switch hashAuthCode from SHA-256 to HMAC-SHA256
   keyed with BROKER_PRIVATE_KEY
6. Bot token plaintext in KV: encrypt with nacl.secretbox using key
   derived from BROKER_PRIVATE_KEY, decrypt on read
7. HTTPS enforcement: add protocol check in forwardEvent()
8. Rate limiting: add TODO comment noting Phase 3 requirement
9. zeroBytes docs: clarify best-effort cleanup, JS string limitation

Tests: 54 → 64 (10 new tests covering all security changes)
@benvinegar benvinegar merged commit 48ad560 into main Feb 19, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants