Create docs/specs/006-linq-integration.md
ab2c9b55638a jacobcole 2026-04-23 1 file
new file mode 100644
index 0000000..f6ae3c7
@@ -0,0 +1,85 @@
+---
+visibility: public
+---
+
+# Spec 006 — Linq integration
+
+**Status:** Draft
+**Related:** [PRD FR-1..FR-4](../prd/001-picortex-v1.md#linq-integration), [ADR-0004](../adrs/0004-linq-primary-channel.md), [Wiki: linq-protocol](../wiki/linq-protocol.md)
+
+## Goal
+
+Speak fluent Linq, both inbound (webhook ingest) and outbound (partner API), with HMAC signing parity. Testable end-to-end against `linq-sim` without needing real phone numbers.
+
+## Inbound
+
+`POST /api/linq/inbound`
+
+Headers:
+- `Linq-Signature: t=<unix_ts>,s=<hex_hmac_sha256>` (Cortex's exact shape)
+- `Linq-Event-Id: <uuid>`
+- `Content-Type: application/json`
+
+Verification:
+1. Parse `t` and `s` from `Linq-Signature`.
+2. Compute `hmac_sha256("{t}.{raw_body}", LINQ_WEBHOOK_SECRET)`; constant-time compare.
+3. Reject if timestamp skew > 5 min (replay guard).
+4. Dedup via `Linq-Event-Id` seen within 24h.
+5. Log with `X-Request-ID` tagged to event id.
+
+Supported event types (from linq-sim):
+```
+message.received message.delivered message.read message.edited message.failed
+reaction.added reaction.removed
+chat.typing_indicator.started chat.typing_indicator.stopped
+chat.created chat.updated chat.group_name_updated
+participant.added participant.removed
+```
+
+**New for v1:** `message.received` with `data.reply_to_message_id` set (requires [S2 linq-sim PR](../plans/2026-04-23-initial-roadmap.md#s2-linq-sim-thread-support)).
+
+Dispatch table: each event → a handler in `src/channels/linq/handlers/*.ts`. Handlers are pure wrt Linq — they call into the core chat service.
+
+## Outbound
+
+Single client at `src/channels/linq/client.ts`:
+
+```ts
+class LinqClient {
+ sendMessage({ chatId, text, replyToMessageId?, attachments? })
+ createChat({ participants })
+ getChat({ chatId })
+ addParticipant({ chatId, participant })
+ removeParticipant({ chatId, participant })
+ updateChat({ chatId, patch })
+}
+```
+
+Base URL from `LINQ_BASE_URL`. For dev, points at linq-sim (`http://127.0.0.1:8447`). Calls to sim still succeed but are captured for inspection in the sim UI.
+
+Retry: exponential backoff up to 3 attempts on 5xx. 4xx never retried.
+
+## Channel abstraction
+
+```ts
+interface Channel {
+ name: string
+ verifyInbound(req): Promise<ParsedEvent>
+ send(msg: OutboundMessage): Promise<{id: string}>
+ supports(feature: "reactions" | "threads" | "typing"): boolean
+}
+```
+
+`LinqChannel` implements this. Future `OpenChatChannel` ([Wiki: openchat-adapter](../wiki/openchat-adapter.md)) will too.
+
+## Testing
+
+- **Unit:** HMAC signer/verifier, timestamp skew, signature format parsing.
+- **Integration:** linq-sim roundtrip — send via sim's admin UI, picortex handles, responds via sim-captured `/api/partner/v3/sendMessage`.
+- **E2E:** full conversation against linq-sim.
+
+## Open questions
+
+- OQ1: Do we store inbound raw event JSON or normalized? (Store raw + normalized both — Cortex does this.)
+- OQ2: What attachment types must we support at v0.1? (Text-only is fine; images/voice memos are v0.2.)
+- OQ3: Linq's rate limits?
\ No newline at end of file