The instance route-split suite (from #2733) keeps src/webhook-server.test.ts;
this branch's raw-route suite moves to src/webhook-server-raw.test.ts —
incompatible lifecycle setups (fixed port + afterEach vs random port +
afterAll) make a single merged file wrong. webhook-server.ts auto-merge
verified: raw routes take dispatch priority, stop clears both maps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a RawWebhookHandler registry alongside the Chat SDK adapter routes
so modules can mount plain Node handlers at /webhook/{path} on the
shared server instead of editing webhook-server.ts or standing up a
second HTTP server on another port. Raw routes dispatch ahead of
adapter routes, handler throws surface as a 500, and stopWebhookServer
clears the registry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ChatSdkBridgeConfig gains `instance`. The bridge keeps channelType =
adapter.name (semantic platform identity is untouched) and threads the
instance into three places:
- Registry identity: bridge.name / bridge.instance follow config.instance.
- Chat SDK state: SqliteStateAdapter takes an optional namespace and
prefixes every key at a single choke point (k()). All bridges share the
chat_sdk_* tables and two same-platform instances see identical
thread/message ids — without the namespace, the SDK's
dedupe:${adapter.name}:${message.id} key makes the second bot silently
drop every message the first processed, locks serialize across bots, and
subscriptions leak engagement. The namespace applies ONLY when instance
is set AND differs from adapter.name: the default instance stays on the
legacy UNPREFIXED keyspace byte-identically, so live installs' existing
subscriptions/kv/locks/lists rows are never orphaned. enqueue does not
prefix (appendToList does) — layout is ns:queue:<tid>; acquireLock
returns the raw threadId and release/extend re-apply k() at their SQL
sites.
- Webhook route: registerWebhookAdapter(chat, adapterName, routingPath =
adapterName) splits the URL segment from the chat.webhooks handler key,
so each same-platform instance gets its own URL (and signing secret).
Signature adopted verbatim from PR #2617 (credit @davekim917's #1804
prototype); the handler body needed zero change — dispatch already read
entry.adapterName, not the route key.
Instance names are validated URL-safe (no '/', '?', ':' or whitespace) at
bridge construction: the route regex is [^/?]+ and ':' is the namespace
delimiter. The Chat instance's inner adapters map stays keyed adapter.name
(the SDK resolves adapters via channelId.split(':')[0] and serializes by
adapter.name) — instance identity lives entirely outside the Chat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aligns with upstream feat/chat-sdk-integration pattern: regex-based
routing (/webhook/{adapterName}), response streaming, cleanup function.
Updates Slack and Teams skill docs to match /webhook/{name} convention
used by all other v2 channel skills.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>