serverless-ircd 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/deploy-aws.yml +1 -3
  3. package/.github/workflows/deploy-cf-tcp.yml +87 -0
  4. package/.github/workflows/deploy-cf.yml +1 -1
  5. package/.node-version +1 -0
  6. package/.nvmrc +1 -0
  7. package/CHANGELOG.md +258 -18
  8. package/README.md +72 -30
  9. package/apps/aws-stack/README.md +2 -2
  10. package/apps/aws-stack/bin/aws.ts +7 -0
  11. package/apps/aws-stack/package.json +4 -4
  12. package/apps/aws-stack/src/aws-stack.ts +118 -6
  13. package/apps/aws-stack/tests/stack.test.ts +98 -3
  14. package/apps/cf-tcp-container/Dockerfile +69 -0
  15. package/apps/cf-tcp-container/package.json +34 -0
  16. package/apps/cf-tcp-container/src/config-loader.ts +145 -0
  17. package/apps/cf-tcp-container/src/container-do.ts +38 -0
  18. package/apps/cf-tcp-container/src/container-server.ts +363 -0
  19. package/apps/cf-tcp-container/src/main.ts +77 -0
  20. package/apps/cf-tcp-container/src/persistence.ts +144 -0
  21. package/apps/cf-tcp-container/src/worker.ts +41 -0
  22. package/apps/cf-tcp-container/terraform/provider.tf +24 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
  25. package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
  26. package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
  27. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
  28. package/apps/cf-tcp-container/tsconfig.build.json +17 -0
  29. package/apps/cf-tcp-container/tsconfig.test.json +15 -0
  30. package/apps/cf-tcp-container/vitest.config.ts +26 -0
  31. package/apps/cf-tcp-container/wrangler.toml +63 -0
  32. package/apps/cf-worker/package.json +1 -1
  33. package/apps/cf-worker/scripts/smoke.mjs +0 -0
  34. package/apps/cf-worker/src/worker.ts +8 -2
  35. package/apps/cf-worker/tests/smoke.test.ts +1 -0
  36. package/apps/cf-worker/wrangler.test.toml +11 -1
  37. package/apps/cf-worker/wrangler.toml +69 -19
  38. package/apps/local-cli/package.json +1 -1
  39. package/apps/local-cli/src/config-loader.ts +11 -0
  40. package/apps/local-cli/src/main.ts +1 -1
  41. package/apps/local-cli/src/server.ts +46 -3
  42. package/apps/local-cli/tests/e2e.test.ts +52 -0
  43. package/package.json +19 -16
  44. package/packages/aws-adapter/package.json +3 -3
  45. package/packages/aws-adapter/src/account-store.ts +121 -0
  46. package/packages/aws-adapter/src/admission.ts +74 -0
  47. package/packages/aws-adapter/src/aws-runtime.ts +124 -34
  48. package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
  49. package/packages/aws-adapter/src/config-loader.ts +62 -0
  50. package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
  51. package/packages/aws-adapter/src/handlers/connect.ts +64 -8
  52. package/packages/aws-adapter/src/handlers/default.ts +43 -2
  53. package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
  54. package/packages/aws-adapter/src/handlers/index.ts +120 -9
  55. package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
  56. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  57. package/packages/aws-adapter/src/index.ts +13 -0
  58. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
  59. package/packages/aws-adapter/tests/account-store.test.ts +279 -0
  60. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  61. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  62. package/packages/aws-adapter/tests/config-loader.test.ts +75 -0
  63. package/packages/aws-adapter/tests/connect.test.ts +78 -0
  64. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
  65. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  66. package/packages/aws-adapter/tests/handlers.test.ts +194 -47
  67. package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
  68. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  69. package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
  70. package/packages/aws-adapter/tests/transactions.test.ts +25 -20
  71. package/packages/cf-adapter/package.json +1 -1
  72. package/packages/cf-adapter/src/cf-runtime.ts +40 -22
  73. package/packages/cf-adapter/src/channel-do.ts +40 -1
  74. package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
  75. package/packages/cf-adapter/src/config-loader.ts +62 -0
  76. package/packages/cf-adapter/src/connection-do.ts +181 -31
  77. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  78. package/packages/cf-adapter/src/env.ts +58 -8
  79. package/packages/cf-adapter/src/index.ts +11 -9
  80. package/packages/cf-adapter/tests/cf-harness.ts +11 -1
  81. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
  82. package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
  83. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  84. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  85. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  86. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  87. package/packages/cf-adapter/tests/connection-do.test.ts +40 -0
  88. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  89. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  90. package/packages/cf-adapter/tests/worker/main.ts +9 -1
  91. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
  92. package/packages/cf-adapter/wrangler.test.toml +18 -1
  93. package/packages/in-memory-runtime/package.json +1 -1
  94. package/packages/in-memory-runtime/src/in-memory-runtime.ts +14 -28
  95. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
  96. package/packages/irc-core/package.json +8 -2
  97. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  98. package/packages/irc-core/src/caps/capabilities.ts +23 -2
  99. package/packages/irc-core/src/case-fold.ts +64 -0
  100. package/packages/irc-core/src/cloak.ts +1 -1
  101. package/packages/irc-core/src/commands/cap.ts +8 -1
  102. package/packages/irc-core/src/commands/index.ts +11 -0
  103. package/packages/irc-core/src/commands/invite.ts +6 -9
  104. package/packages/irc-core/src/commands/ison.ts +61 -0
  105. package/packages/irc-core/src/commands/isupport.ts +1 -1
  106. package/packages/irc-core/src/commands/join.ts +4 -3
  107. package/packages/irc-core/src/commands/kick.ts +4 -11
  108. package/packages/irc-core/src/commands/list.ts +5 -4
  109. package/packages/irc-core/src/commands/mode.ts +5 -9
  110. package/packages/irc-core/src/commands/motd-lines.ts +127 -0
  111. package/packages/irc-core/src/commands/motd.ts +6 -110
  112. package/packages/irc-core/src/commands/oper.ts +151 -0
  113. package/packages/irc-core/src/commands/quit.ts +12 -0
  114. package/packages/irc-core/src/commands/registration.ts +26 -19
  115. package/packages/irc-core/src/commands/sasl.ts +72 -9
  116. package/packages/irc-core/src/commands/server-info.ts +129 -0
  117. package/packages/irc-core/src/commands/tagmsg.ts +205 -0
  118. package/packages/irc-core/src/commands/userhost.ts +84 -0
  119. package/packages/irc-core/src/commands/whowas.ts +113 -0
  120. package/packages/irc-core/src/config.ts +84 -3
  121. package/packages/irc-core/src/credential-hashing.ts +124 -0
  122. package/packages/irc-core/src/effects.ts +6 -29
  123. package/packages/irc-core/src/index.ts +3 -0
  124. package/packages/irc-core/src/ports.ts +240 -7
  125. package/packages/irc-core/src/protocol/numerics.ts +14 -8
  126. package/packages/irc-core/src/types.ts +60 -1
  127. package/packages/irc-core/tests/account-store.test.ts +131 -0
  128. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  129. package/packages/irc-core/tests/case-fold.test.ts +80 -0
  130. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  131. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  132. package/packages/irc-core/tests/commands/kick.test.ts +15 -0
  133. package/packages/irc-core/tests/commands/oper.test.ts +257 -0
  134. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  135. package/packages/irc-core/tests/commands/registration.test.ts +256 -19
  136. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  137. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  138. package/packages/irc-core/tests/commands/tagmsg.test.ts +662 -0
  139. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  140. package/packages/irc-core/tests/commands/who.test.ts +22 -4
  141. package/packages/irc-core/tests/commands/whois.test.ts +8 -1
  142. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  143. package/packages/irc-core/tests/config.test.ts +170 -1
  144. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  145. package/packages/irc-core/tests/effects.test.ts +0 -27
  146. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  147. package/packages/irc-core/tests/numerics.test.ts +12 -0
  148. package/packages/irc-core/tests/types.test.ts +35 -1
  149. package/packages/irc-core/tsconfig.build.json +1 -1
  150. package/packages/irc-core/tsconfig.test.json +1 -1
  151. package/packages/irc-server/package.json +1 -1
  152. package/packages/irc-server/src/actor.ts +182 -14
  153. package/packages/irc-server/src/dispatch.ts +0 -3
  154. package/packages/irc-server/src/index.ts +10 -2
  155. package/packages/irc-server/src/routing.ts +21 -0
  156. package/packages/irc-server/src/transport.ts +101 -0
  157. package/packages/irc-server/tests/actor.test.ts +617 -1
  158. package/packages/irc-server/tests/dispatch.test.ts +0 -17
  159. package/packages/irc-server/tests/routing.test.ts +7 -0
  160. package/packages/irc-server/tests/transport.test.ts +230 -0
  161. package/packages/irc-test-support/package.json +1 -1
  162. package/packages/irc-test-support/src/harness.ts +44 -9
  163. package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
  164. package/packages/irc-test-support/src/index.ts +3 -0
  165. package/packages/irc-test-support/src/scenarios.ts +132 -2
  166. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  167. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  168. package/pnpm-workspace.yaml +9 -0
  169. package/tools/ci-hardening/package.json +1 -1
  170. package/tools/package.json +4 -0
  171. package/tools/seed-aws-accounts.ts +79 -0
  172. package/tools/seed-cf-accounts.ts +104 -0
  173. package/tools/tcp-ws-forwarder/package.json +1 -1
  174. package/AGENTS.md +0 -5
  175. package/MOTD.txt +0 -3
  176. package/PLAN-FIXES.md +0 -358
  177. package/PLAN.md +0 -420
  178. package/dashboards/cloudwatch-irc.json +0 -139
  179. package/docs/AWS-Adapter-Architecture.md +0 -494
  180. package/docs/AWS-Deployment.md +0 -1107
  181. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  182. package/docs/Home.md +0 -1
  183. package/docs/Observability.md +0 -87
  184. package/docs/PlanIRCv3Websocket.md +0 -489
  185. package/docs/PlanWebClient.md +0 -451
  186. package/docs/Release-Process.md +0 -440
  187. package/progress.md +0 -107
  188. package/tickets.md +0 -2485
  189. package/webircgateway/LICENSE +0 -201
  190. package/webircgateway/Makefile +0 -44
  191. package/webircgateway/README.md +0 -134
  192. package/webircgateway/config.conf.example +0 -135
  193. package/webircgateway/go.mod +0 -16
  194. package/webircgateway/go.sum +0 -89
  195. package/webircgateway/main.go +0 -118
  196. package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
  197. package/webircgateway/pkg/identd/identd.go +0 -86
  198. package/webircgateway/pkg/identd/rpcclient.go +0 -59
  199. package/webircgateway/pkg/irc/isupport.go +0 -56
  200. package/webircgateway/pkg/irc/message.go +0 -217
  201. package/webircgateway/pkg/irc/state.go +0 -79
  202. package/webircgateway/pkg/proxy/proxy.go +0 -129
  203. package/webircgateway/pkg/proxy/server.go +0 -237
  204. package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
  205. package/webircgateway/pkg/webircgateway/client.go +0 -741
  206. package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
  207. package/webircgateway/pkg/webircgateway/config.go +0 -385
  208. package/webircgateway/pkg/webircgateway/gateway.go +0 -278
  209. package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
  210. package/webircgateway/pkg/webircgateway/hooks.go +0 -152
  211. package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
  212. package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
  213. package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
  214. package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
  215. package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
  216. package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
  217. package/webircgateway/pkg/webircgateway/utils.go +0 -147
  218. package/webircgateway/plugins/example/plugin.go +0 -11
  219. package/webircgateway/plugins/stats/plugin.go +0 -52
  220. package/webircgateway/staticcheck.conf +0 -1
  221. package/webircgateway/webircgateway.svg +0 -3
package/tickets.md DELETED
@@ -1,2485 +0,0 @@
1
- # ServerlessIRCd — Tickets
2
-
3
- Derived from `PLAN.md` §7 Roadmap. Phase 0 (Foundation) is already completed.
4
- Points use the Fibonacci scale (8 points ≈ 1 week for a human developer).
5
- Development follows strict TDD (Red → Green → Refactor). Every ticket starts
6
- with a failing test.
7
-
8
- Status legend: ⬜ pending · 🔄 in-progress · ✅ done · 🚫 blocked
9
-
10
- ---
11
-
12
- ## Phase 1 — Pure protocol engine (no cloud)
13
-
14
- ### TICKET-001 — Core state shapes, `Effect` taxonomy, `dispatch` interpreter
15
- - **Phase:** 1
16
- - **Points:** 5
17
- - **Status:** ✅
18
- - **Dependencies:** —
19
- - **Description:** Define the pure core of `irc-core`: the `IrcState` shapes
20
- (`ConnectionState`, `ChannelState`, `Roster`, `Registry`), the discriminated
21
- union `Effect` taxonomy (`Send`, `Broadcast`, `Disconnect`, `ReserveNick`,
22
- `ChangeNick`, `ReleaseNick`, `ApplyChannelDelta`, `Lookup`…), and the
23
- `Reducer<S>` signature
24
- `(state, msg, ctx) => { state, effects }`. Implement
25
- `dispatch(effects, runtime)` that interprets an `Effect[]` against the
26
- `IrcRuntime` port (port defined here as an interface; concrete runtimes come
27
- later). Add ports `Clock` and `IdFactory` for deterministic time/IDs.
28
- - **Acceptance Criteria:**
29
- - All type files compile under `tsconfig.base.json` strict mode.
30
- - `dispatch` is unit-tested with a fake `IrcRuntime` recording calls;
31
- ordering and dedup of effects verified.
32
- - `Effect` union covers every side-effect referenced in PLAN §2.1.
33
- - 100% coverage on `dispatch.ts`, `effects.ts`, `types.ts`, ports.
34
- - **TDD Outline:**
35
- 1. Red: write a test asserting `dispatch([Send(...), Broadcast(...)])`
36
- calls the fake runtime in order — fails (no `dispatch`).
37
- 2. Green: implement `dispatch` as a thin loop.
38
- 3. Refactor: extract effect handlers into a dispatch table keyed by
39
- effect tag.
40
-
41
- ### TICKET-002 — `PING` / `PONG` reducer
42
- - **Phase:** 1
43
- - **Points:** 1
44
- - **Status:** ✅
45
- - **Dependencies:** TICKET-001
46
- - **Description:** Pure reducer for client `PING` (server replies `PONG`)
47
- and server-initiated `PING` (emits `Send(PING)`; expects client `PONG`).
48
- Idle/PING bookkeeping is the runtime's job; reducer only emits replies.
49
- - **Acceptance Criteria:**
50
- - `PING <token>` → emits `PONG <token>` to sender.
51
- - Missing token still produces a `PONG` (per modern client behavior).
52
- - No state mutation beyond last-seen.
53
- - **TDD Outline:** Red on "ping with token returns matching pong"; green;
54
- cover missing-token + uppercase/lowercase variants.
55
-
56
- ### TICKET-003 — `QUIT` reducer
57
- - **Phase:** 1
58
- - **Points:** 2
59
- - **Status:** ✅
60
- - **Dependencies:** TICKET-001
61
- - **Description:** Pure reducer for `QUIT [reason]`. Emits `Broadcast(QUIT)`
62
- to every channel the user is in, `Disconnect`, `ReleaseNick`, and a
63
- `ChannelDelta` removing the user from each channel roster.
64
- - **Acceptance Criteria:**
65
- - All joined channels receive the QUIT message.
66
- - Nick is released in the registry effect.
67
- - Reason defaults to the connection's configured quit message.
68
- - Idempotent if connection had not yet registered.
69
- - **TDD Outline:** Red on "quit broadcasts to two channels"; green; cover
70
- no-channels case + unregistered-connection case.
71
-
72
- ### TICKET-004 — Registration reducers: `NICK` / `USER` / `PASS` (core)
73
- - **Phase:** 1
74
- - **Points:** 5
75
- - **Status:** ✅
76
- - **Dependencies:** TICKET-001
77
- - **Description:** Pure reducers for the registration handshake
78
- (pre-`CAP`). `NICK` reserves via `ReserveNick` effect and validates
79
- nick grammar (`nick = [a-zA-Z][\w-\[\]\\\`\^{|}]*`). `USER` stores
80
- username/realname. `PASS` stashes the server password for validation.
81
- On completion emits welcome numerics `001`–`005` placeholder (005 filled
82
- in TICKET-024), `375`/`372`/`376` MOTD placeholders.
83
- - **Acceptance Criteria:**
84
- - Invalid nick → `432`/`433` as appropriate.
85
- - Already-registered → `462`.
86
- - Welcome sequence only fires once NICK *and* USER are received.
87
- - `PASS` mismatch → close with effect (server-password enforcement
88
- lives in TICKET-030; this reducer only records the attempt).
89
- - **TDD Outline:** Red on "nick + user → emits 001..005"; green; then add
90
- each error path as its own test.
91
-
92
- ### TICKET-005 — `JOIN` reducer
93
- - **Phase:** 1
94
- - **Points:** 5
95
- - **Status:** ✅
96
- - **Dependencies:** TICKET-001, TICKET-004
97
- - **Description:** Pure reducer for `JOIN <chan>{,<chan>} [<key>{,<key>}]`.
98
- Validates channel name grammar & allowed prefixes; honors `k` (key), `i`
99
- (invite-only), `l` (limit), `b` (ban) on channel state. Emits
100
- `Broadcast(JOIN)` to the channel including the joiner, followed by the
101
- joiner's `353` NAMES list and `366` end-of-names.
102
- - **Acceptance Criteria:**
103
- - Join cascade produces the correct RFC-compliant message order.
104
- - `JOIN 0` parts all channels (legacy edge case) — tested.
105
- - Channel key checked against channel state's `k`.
106
- - Limits: max channels per user (configurable via `ctx`).
107
- - **TDD Outline:** Red on "join #foo → broadcasts JOIN + sends 353/366";
108
- green; then one test per rejection (`403`, `471`, `473`, `475`,
109
- `405`).
110
-
111
- ### TICKET-006 — `PART` reducer
112
- - **Phase:** 1
113
- - **Points:** 2
114
- - **Status:** ✅
115
- - **Dependencies:** TICKET-001, TICKET-005
116
- - **Description:** Pure reducer for `PART <chan>{,<chan>} [:reason]`.
117
- - **Acceptance Criteria:**
118
- - Not on channel → `441`.
119
- - No such channel → `403`.
120
- - Broadcasts `PART` to channel; updates roster via `ChannelDelta`.
121
- - **TDD Outline:** Red on "part with reason"; green; then negative paths.
122
-
123
- ### TICKET-007 — `PRIVMSG` / `NOTICE` reducer (channel + private)
124
- - **Phase:** 1
125
- - **Points:** 3
126
- - **Status:** ✅
127
- - **Dependencies:** TICKET-001, TICKET-005
128
- - **Description:** Pure reducer for `PRIVMSG`/`NOTICE` to channels and to
129
- users. Channel target fans out via `Broadcast`; user target routes via
130
- `LookupNick` → `Send`. Enforces `+n` (no external), `+m` (moderated),
131
- `+b` (ban). No echoes unless `echo-message` cap is set (handled in
132
- TICKET-014). `NOTICE` MUST NEVER produce numeric error replies
133
- (RFC-critical).
134
- - **Acceptance Criteria:**
135
- - Channel PRIVMSG broadcasts to all members except sender (unless
136
- echo-message).
137
- - Private PRIVMSG to offline user → `401` for PRIVMSG; silent for NOTICE.
138
- - 512-byte truncation handled in parser (TICKET-001 foundation); reducer
139
- enforces max target count.
140
- - **TDD Outline:** Red on channel PRIVMSG; green; then NOTICE silence path
141
- + ban/+n/+m rejection paths.
142
-
143
- ### TICKET-008 — `TOPIC` reducer
144
- - **Phase:** 1
145
- - **Points:** 3
146
- - **Status:** ✅
147
- - **Dependencies:** TICKET-001, TICKET-005
148
- - **Description:** Pure reducer for `TOPIC <chan> [:topic]`. Reads emit
149
- `331` (no topic) or `332` + `333` (setter/time). Writes honor `+t`
150
- (op-only) → `482` for non-ops.
151
- - **Acceptance Criteria:**
152
- - Not on channel → `442`.
153
- - No such channel → `403`.
154
- - Topic length cap configurable in `ctx`.
155
- - **TDD Outline:** Red on set topic; green; then read variants and `+t`
156
- enforcement.
157
-
158
- ### TICKET-009 — `NAMES` reducer
159
- - **Phase:** 1
160
- - **Points:** 2
161
- - **Status:** ✅
162
- - **Dependencies:** TICKET-001, TICKET-005
163
- - **Description:** Pure reducer for `NAMES [<chan>{,<chan>}]`. Emits
164
- `353`/`366`. Honors `+s` (secret) / `+p` (private): non-members cannot
165
- list.
166
- - **Acceptance Criteria:**
167
- - Multi-prefix output depends on the client's `multi-prefix` cap
168
- (plumbed through `ctx`).
169
- - No args → all visible channels + a final "end of list" `366` per
170
- modern clients.
171
- - **TDD Outline:** Red on names of joined channel; green; then secret
172
- channel exclusion.
173
-
174
- ### TICKET-010 — `MOTD` reducer
175
- - **Phase:** 1
176
- - **Points:** 1
177
- - **Status:** ✅
178
- - **Dependencies:** TICKET-001, TICKET-004
179
- - **Description:** Pure reducer for `MOTD`. Reads the MOTD from a
180
- `MotdProvider` port (so adapters can supply from KV/D1/SecretsManager).
181
- Emits `375`/`372`×N/`376`, or `422` if no MOTD.
182
- - **Acceptance Criteria:**
183
- - Empty MOTD → `422`.
184
- - Long lines split per the 512-byte (incl. tags) limit.
185
- - **TDD Outline:** Red on "motd with 3 lines emits 3×372"; green.
186
-
187
- ### TICKET-011 — `MODE` reducer (channel + user)
188
- - **Phase:** 1
189
- - **Points:** 8
190
- - **Status:** ⬜
191
- - **Dependencies:** TICKET-001, TICKET-005
192
- - **Description:** Pure reducer for `MODE`. Two surfaces: channel modes
193
- (`o v b i k l t n m s p` — op, voice, ban, invite-only, key, limit,
194
- topic-lock, no-external, moderated, secret, private) and user modes
195
- (`i o w s`). Modes parsed into an additive/subtractive delta. Ban masks
196
- produce `367`/`368` list replies.
197
- - **Acceptance Criteria:**
198
- - All channel mode letters tested for set, unset, and invalid.
199
- - Op/voice prefix changes broadcast `MODE` to the channel.
200
- - `l` requires a numeric argument; `k` requires a key arg.
201
- - Non-op attempting mode change → `482`.
202
- - Unknown mode char → `472`.
203
- - **TDD Outline:** Red on simple `+t`; green; then incrementally add modes
204
- one test at a time. Refactor to a mode table-driven parser.
205
-
206
- ### TICKET-012 — `KICK` reducer
207
- - **Phase:** 1
208
- - **Points:** 3
209
- - **Status:** ⬜
210
- - **Dependencies:** TICKET-001, TICKET-005, TICKET-011
211
- - **Description:** Pure reducer for `KICK <chan> <user> [:reason]`.
212
- - **Acceptance Criteria:**
213
- - Not op → `482`.
214
- - Target not on channel → `441`.
215
- - Kicker not on channel → `442`.
216
- - Broadcasts `KICK` then updates roster.
217
- - **TDD Outline:** Red on successful kick; green; then each error path.
218
-
219
- ### TICKET-013 — `INVITE` reducer
220
- - **Phase:** 1
221
- - **Points:** 3
222
- - **Status:** ⬜
223
- - **Dependencies:** TICKET-001, TICKET-005, TICKET-011
224
- - **Description:** Pure reducer for `INVITE`. Sends `341` to inviter and an
225
- `INVITE` notice to the invitee; records pending invite in channel state
226
- so a later `JOIN` bypasses `+i`. Honors channel `+i` requiring inviter
227
- to be a member (operator-only if configured).
228
- - **Acceptance Criteria:**
229
- - Invite to nonexistent channel → `403`.
230
- - Invitee already on channel → `443`.
231
- - Pending invite allows join past `+i` until consumed or expired.
232
- - **TDD Outline:** Red on successful invite; green; then edge cases.
233
-
234
- ### TICKET-014 — `WHO` reducer
235
- - **Phase:** 1
236
- - **Points:** 3
237
- - **Status:** ⬜
238
- - **Dependencies:** TICKET-001, TICKET-005
239
- - **Description:** Pure reducer for `WHO [<mask>] [o]`. Emits `352` per
240
- match then `315`. Honors `+i` (invisible) user mode and `+s` channels.
241
- - **Acceptance Criteria:**
242
- - `WHO #chan` lists members.
243
- - Invisible users only shown if the requester shares a channel.
244
- - Operator flag (`*`) and away flag (`H`/`G`) correct in `352`.
245
- - **TDD Outline:** Red on WHO of a joined channel; green; then `+i`
246
- filtering.
247
-
248
- ### TICKET-015 — `WHOIS` reducer
249
- - **Phase:** 1
250
- - **Points:** 3
251
- - **Status:** ⬜
252
- - **Dependencies:** TICKET-001, TICKET-004
253
- - **Description:** Pure reducer for `WHOIS <nick>{,<nick>}`. Emits
254
- `311`/`319`/`312`/`317`/`318` (and `330` account when the
255
- `account-tag`/`extended-join` data is available).
256
- - **Acceptance Criteria:**
257
- - Unknown nick → `401` then `318`.
258
- - Idle seconds & signon time sourced from `ConnSnapshot`.
259
- - Channels list truncated / hidden per `+s`/`+p`.
260
- - **TDD Outline:** Red on WHOIS of registered user; green; then unknown
261
- nick and secret-channel filtering.
262
-
263
- ### TICKET-016 — `LIST` reducer
264
- - **Phase:** 1
265
- - **Points:** 2
266
- - **Status:** ⬜
267
- - **Dependencies:** TICKET-001, TICKET-005
268
- - **Description:** Pure reducer for `LIST [<chan>{,<chan>}]`. Emits `321`,
269
- one `322` per channel, `323`. Secret channels hidden from non-members.
270
- - **Acceptance Criteria:**
271
- - `LIST` with no args → all non-secret channels.
272
- - Specific channel list → only those channels.
273
- - Capped result set (configurable) to prevent thundering-herd
274
- (PLAN §10 risk).
275
- - **TDD Outline:** Red on LIST of public channels; green; then `+s`
276
- filtering.
277
-
278
- ### TICKET-017 — IRCv3 `CAP` negotiation (`LS`/`REQ`/`ACK`/`NAK`/`END`/`LIST`/`NEW`/`DEL`)
279
- - **Phase:** 1
280
- - **Points:** 3
281
- - **Status:** ⬜
282
- - **Dependencies:** TICKET-001, TICKET-004
283
- - **Description:** Pure reducer for the `CAP` command and the
284
- `capabilities.ts` capability table. Maintains connection's negotiated cap
285
- set in `ConnectionState`.
286
- - **Acceptance Criteria:**
287
- - `CAP LS` → `* LS :<caps>` (multiline if needed).
288
- - `CAP REQ` validates each requested cap; ACKs supported, NAKs unknown.
289
- - `CAP END` triggers the welcome sequence if registration is otherwise
290
- complete.
291
- - All caps from PLAN §3 listed in the table (their *behaviors* ship in
292
- later tickets; the table itself ships here as data).
293
- - **TDD Outline:** Red on `CAP LS` response; green; then ACK/NAK matrix.
294
-
295
- ### TICKET-018 — IRCv3 `message-tags` + `server-time`
296
- - **Phase:** 1
297
- - **Points:** 3
298
- - **Status:** ⬜
299
- - **Dependencies:** TICKET-017
300
- - **Description:** Implement `message-tags` (client `+tag` send +
301
- server-tag emission) and `server-time` (prepend `@time=...` on all
302
- outbound messages). Hook into serializer (TICKET-001 foundation).
303
- - **Acceptance Criteria:**
304
- - Time format is ISO-8601 with milliseconds (`yyyy-MM-ddTHH:mm:ss.sssZ`).
305
- - Tag values escaped/unescaped per spec.
306
- - Tags only emitted to clients who negotiated the relevant cap.
307
- - 512-byte limit includes tags for clients that have `message-tags`.
308
- - **TDD Outline:** Red on "broadcast PRIVMSG to server-time client
309
- includes @time"; green; then property-test round-trip of tag escaping.
310
-
311
- ### TICKET-019 — IRCv3 `echo-message`
312
- - **Phase:** 1
313
- - **Points:** 1
314
- - **Status:** ⬜
315
- - **Dependencies:** TICKET-017, TICKET-007
316
- - **Description:** For clients with `echo-message`, the reducer emits a
317
- `Send` back to the originator mirroring their own PRIVMSG/NOTICE/TAGMSG.
318
- - **Acceptance Criteria:**
319
- - Without the cap: no echo.
320
- - With the cap: one echo per message, after the broadcast.
321
- - **TDD Outline:** Red on echo path; green.
322
-
323
- ### TICKET-020 — IRCv3 `multi-prefix`
324
- - **Phase:** 1
325
- - **Points:** 2
326
- - **Status:** ⬜
327
- - **Dependencies:** TICKET-009, TICKET-017
328
- - **Description:** NAMES `353` and `WHO` `352` outputs include all
329
- applicable prefixes (`@+`) for clients that negotiated `multi-prefix`;
330
- legacy clients get only the highest-priority prefix.
331
- - **Acceptance Criteria:**
332
- - Op+voice user shown as `@+nick` to multi-prefix clients.
333
- - **TDD Outline:** Red on NAMES with both caps; green.
334
-
335
- ### TICKET-021 — IRCv3 `extended-join`
336
- - **Phase:** 1
337
- - **Points:** 1
338
- - **Status:** ⬜
339
- - **Dependencies:** TICKET-005, TICKET-017
340
- - **Description:** `JOIN` messages for clients with `extended-join` carry
341
- `account` and `realname`: `:nick JOIN #chan account :realname`. `_`
342
- account = not logged in.
343
- - **Acceptance Criteria:**
344
- - Account name plumbed from SASL auth (later ticket) or `_`.
345
- - **TDD Outline:** Red on extended JOIN format; green.
346
-
347
- ### TICKET-022 — IRCv3 `chghost`
348
- - **Phase:** 1
349
- - **Points:** 2
350
- - **Status:** ⬜
351
- - **Dependencies:** TICKET-017
352
- - **Description:** On user/host change (cloaking in TICKET-030; SETNAME
353
- not in v1 scope), emit `CHGHOST` to peers in shared channels who have
354
- the cap.
355
- - **Acceptance Criteria:**
356
- - Only emitted to cap-enabled peers.
357
- - Only emitted after registration completes (never during).
358
- - **TDD Outline:** Red on hostmask change broadcast; green.
359
-
360
- ### TICKET-023 — IRCv3 `away-notify`
361
- - **Phase:** 1
362
- - **Points:** 2
363
- - **Status:** ✅
364
- - **Dependencies:** TICKET-017
365
- - **Description:** `AWAY` reducer (reuses logic from core): on AWAY set,
366
- emit `RPL_AWAY` numerics to PRIVMSG senders AND `AWAY` messages to
367
- cap-enabled peers in shared channels. On unset, emit `AWAY` with empty
368
- reason.
369
- - **Acceptance Criteria:**
370
- - Without cap: only numeric replies to senders.
371
- - With cap: relayed AWAY/UNAWAY events to shared-channel peers.
372
- - **TDD Outline:** Red on AWAY set with cap peer; green.
373
-
374
- ### TICKET-024 — IRCv3 `invite-notify`
375
- - **Phase:** 1
376
- - **Points:** 1
377
- - **Status:** ✅
378
- - **Dependencies:** TICKET-013, TICKET-017
379
- - **Description:** Channel members with `invite-notify` receive a copy of
380
- `INVITE` messages issued to the channel, in addition to the invitee.
381
- - **Acceptance Criteria:**
382
- - Only cap-enabled members get the copy.
383
- - Invitee still gets exactly one INVITE.
384
- - **TDD Outline:** Red on extra INVITE fanout; green.
385
-
386
- ### TICKET-025 — IRCv3 `batch`
387
- - **Phase:** 1
388
- - **Points:** 5
389
- - **Status:** ✅
390
- - **Dependencies:** TICKET-017, TICKET-018
391
- - **Description:** Wrap multi-line server responses (`353` NAMES, JOIN
392
- + 353 + 366 sequence, NETJOIN) in `BATCH` for clients that negotiate it.
393
- Netbatch ref id allocation via `IdFactory`.
394
- - **Acceptance Criteria:**
395
- - `BATCH +id type [:args]` … `BATCH -id` framing correct.
396
- - Nested batches supported.
397
- - Without cap: no change to output.
398
- - **TDD Outline:** Red on NAMES wrapped in batch; green; then nested.
399
-
400
- ### TICKET-026 — SASL authentication (`PLAIN` via `AccountStore`, `EXTERNAL` stubbed)
401
- - **Phase:** 1
402
- - **Points:** 5
403
- - **Status:** ⬜
404
- - **Dependencies:** TICKET-017, TICKET-004
405
- - **Description:** Implement `AUTHENTICATE` / `901`–`908` numerics. Define
406
- an `AccountStore` port (`verify(mech, payload) →
407
- { ok, account } | { ok: false, reason }`). Implement `PLAIN`. `EXTERNAL`
408
- is recognized and rejected (`904`) until mTLS lands (v1.x).
409
- - **Acceptance Criteria:**
410
- - `CAP REQ sasl` then `AUTHENTICATE PLAIN` flow completes with `900`/
411
- `903` on success or `904`/`907` on failure.
412
- - Account name recorded on connection for `extended-join`/`account-tag`.
413
- - Abort path (`AUTHENTICATE` after `CAP END`) → `907`.
414
- - **TDD Outline:** Red on successful PLAIN flow with a fake AccountStore;
415
- green; then each failure numeric.
416
-
417
- ### TICKET-027 — `isupport` (`005`) generator
418
- - **Phase:** 1
419
- - **Points:** 2
420
- - **Status:** ⬜
421
- - **Dependencies:** TICKET-004, TICKET-011
422
- - **Description:** Pure function `generateIsupport(serverConfig, connCaps)
423
- → RawLine[]` producing the `005` tokens
424
- (`CHANTYPES=#`, `CHANMODES=…`, `PREFIX=(ov)@+`, `MODES`, `NICKLEN`,
425
- `CHANNELLEN`, `CASEMAPPING=ascii|rfc1459`, `NETWORK=…`, etc.). Wires into
426
- the welcome sequence emitted by TICKET-004.
427
- - **Acceptance Criteria:**
428
- - Tokens derived from config, not hard-coded.
429
- - Multi-line `005` split correctly; final token `:are supported by
430
- this server`.
431
- - **TDD Outline:** Red on basic token set; green; then each configurable
432
- knob.
433
-
434
- ### TICKET-028 — Flood control (token bucket) as pure reducer wrapper
435
- - **Phase:** 1
436
- - **Points:** 3
437
- - **Status:** ⬜
438
- - **Dependencies:** TICKET-001
439
- - **Description:** Higher-order reducer wrapper that consults a
440
- per-connection token bucket (refilled using injected `Clock`). Over-rate
441
- messages either queue or trigger `Disconnect(excess flood)` based on
442
- policy. Bucket state stored on `ConnectionState`.
443
- - **Acceptance Criteria:**
444
- - Under-limit messages pass through unchanged.
445
- - Burst over limit triggers `Disconnect` with the canonical
446
- "Excess Flood" reason after the configured threshold.
447
- - Deterministic: same Clock input → same decision.
448
- - **TDD Outline:** Red on "10 PRIVMSG in 1ms disconnects"; green; then
449
- parameterize thresholds.
450
-
451
- ### TICKET-059 — Wire `WHOIS` into the `ConnectionActor` dispatcher
452
- - **Phase:** 1
453
- - **Points:** 2
454
- - **Status:** ✅
455
- - **Dependencies:** TICKET-015 (reducer, ✅), TICKET-030 (actor, ✅)
456
- - **Description:** The `WHOIS` reducer is fully implemented and tested
457
- (TICKET-015), but `packages/irc-server/src/actor.ts:182-236` deliberately
458
- does **not** route `WHOIS` — an inline comment at `actor.ts:221-222`
459
- reads *"WHOIS needs state the actor doesn't currently expose (full
460
- connection map). Falls through to the default 421 path."* As a result a
461
- client sending `WHOIS` receives `421 ERR_UNKNOWNCOMMAND` (verified by
462
- `packages/irc-server/tests/actor.test.ts:496-503`). This ticket adds the
463
- missing `case 'WHOIS'` and exposes whatever connection-map state the
464
- reducer needs (e.g. via `IrcRuntime`/`ConnSnapshot` lookup) so the
465
- existing reducer is actually reachable.
466
- - **Acceptance Criteria:**
467
- - `WHOIS <nick>` returns the `311`/`319`/`312`/`317`/`318` sequence
468
- (and `401`+`318` for an unknown nick) — never `421`.
469
- - The `actor.ts:221-222` "falls through" comment is removed.
470
- - The test at `actor.test.ts:496-503` asserting `421` for `WHOIS` is
471
- replaced with a test asserting the real `311`+`318` flow.
472
- - `packages/irc-core/tests/commands/whois.test.ts` continues to pass
473
- unchanged (reducer behavior is not modified, only wired in).
474
- - **TDD Outline:** Red on an actor-level test asserting `WHOIS` of a
475
- registered user emits `311` (currently fails — emits `421`); green by
476
- adding the `case` and the state plumbing; then add the unknown-nick
477
- path at the actor level.
478
-
479
- ### TICKET-060 — Wire `AUTHENTICATE` into the `ConnectionActor` dispatcher
480
- - **Phase:** 1
481
- - **Points:** 1
482
- - **Status:** ✅
483
- - **Dependencies:** TICKET-026 (SASL reducer), TICKET-030 (actor, ✅)
484
- - **Description:** The `sasl` capability is advertised to clients as
485
- `sasl=PLAIN,EXTERNAL` (`packages/irc-core/src/caps/capabilities.ts:34`),
486
- but `actor.ts:182-236` has no `case 'AUTHENTICATE'`. Once TICKET-026
487
- ships the SASL reducer and `901`–`908` numerics, this ticket adds the
488
- dispatcher routing so the command is reachable. Until then, any client
489
- that negotiates `sasl` and sends `AUTHENTICATE PLAIN` gets a `421`.
490
- - **Acceptance Criteria:**
491
- - After `CAP REQ sasl` / `CAP ACK`, `AUTHENTICATE PLAIN` is routed to
492
- the SASL reducer (no `421`).
493
- - `actor.ts` contains a `case 'AUTHENTICATE'`.
494
- - Pre-registration `AUTHENTICATE` (before `CAP END`) is handled per the
495
- SASL spec, not rejected as unknown.
496
- - **TDD Outline:** Red on an actor-level test asserting
497
- `AUTHENTICATE PLAIN` (post-`CAP REQ sasl`) does not return `421`;
498
- green by adding the `case`.
499
-
500
- ### TICKET-061 — Wire `AWAY` into the `ConnectionActor` dispatcher
501
- - **Phase:** 1
502
- - **Points:** 1
503
- - **Status:** ✅
504
- - **Dependencies:** TICKET-023 (AWAY reducer), TICKET-030 (actor, ✅)
505
- - **Description:** The `away-notify` capability is advertised to clients
506
- (`packages/irc-core/src/caps/capabilities.ts:26`), and the `RPL_AWAY`
507
- (`301`), `RPL_UNAWAY` (`305`), `RPL_NOWAWAY` (`306`) numerics are
508
- already defined in `packages/irc-core/src/protocol/numerics.ts:20-24`
509
- but never emitted. `actor.ts:182-236` has no `case 'AWAY'`, so a client
510
- sending `AWAY :reason` gets `421`. Once TICKET-023 ships the `AWAY`
511
- reducer, this ticket adds the dispatcher routing so the command is
512
- reachable.
513
- - **Acceptance Criteria:**
514
- - `AWAY :reason` is routed to the AWAY reducer (no `421`); emits
515
- `306 RPL_NOWAWAY`.
516
- - `AWAY` with no params (or empty reason) unsets away; emits
517
- `305 RPL_UNAWAY`.
518
- - `actor.ts` contains a `case 'AWAY'`.
519
- - **TDD Outline:** Red on an actor-level test asserting `AWAY :gone`
520
- does not return `421`; green by adding the `case`; then the unset path.
521
-
522
- ---
523
-
524
- ## Phase 2 — Runtime port + reference impl
525
-
526
- ### TICKET-029 — Define `IrcRuntime` port; implement `in-memory-runtime`
527
- - **Phase:** 2
528
- - **Points:** 5
529
- - **Status:** ⬜
530
- - **Dependencies:** TICKET-001
531
- - **Description:** Lock down the `IrcRuntime` interface (PLAN §2.2) in
532
- `packages/irc-server/src/runtime.ts`. Ship a single-process reference
533
- implementation in `packages/in-memory-runtime` backed by `Map`s and the
534
- injected `Clock`/`IdFactory`. Used by integration tests, the local CLI,
535
- and as the contract spec for CF/AWS.
536
- - **Acceptance Criteria:**
537
- - All `IrcRuntime` methods implemented and unit-tested.
538
- - Nick reservation is race-free within the single-process model (proven
539
- by a stress test using a fake clock).
540
- - `broadcast` skips `except` correctly.
541
- - **TDD Outline:** Red on `reserveNick` uniqueness across two calls;
542
- green; then broadcast/nick-change/getChannelSnapshot.
543
-
544
- ### TICKET-030 — `ConnectionActor`: bytes → framed messages → reducer → dispatch
545
- - **Phase:** 2
546
- - **Points:** 5
547
- - **Status:** ⬜
548
- - **Dependencies:** TICKET-029, TICKET-028
549
- - **Description:** In `packages/irc-server/src/actor.ts`, implement the
550
- per-connection pipeline: receive a WebSocket text frame, split on `\r\n`
551
- (tolerating the single-message-per-frame and joined-frame variants from
552
- PLAN §9), parse, route by command to the right reducer per the
553
- location-of-authority table, apply, and `dispatch` the resulting effects
554
- against the bound `IrcRuntime`.
555
- - **Acceptance Criteria:**
556
- - One message per frame → one reducer invocation.
557
- - Joined frame with N `\r\n` → N invocations, in order.
558
- - Parser failures produce `421` and continue (never crash the actor).
559
- - Flood-control wrapper applied per TICKET-028.
560
- - **TDD Outline:** Red on "text frame with one PRIVMSG results in one
561
- broadcast"; green; then multi-line frame + malformed line.
562
-
563
- ### TICKET-031 — `apps/local-cli`: runnable WebSocket server using in-memory runtime
564
- - **Phase:** 2
565
- - **Points:** 3
566
- - **Status:** ⬜
567
- - **Dependencies:** TICKET-029, TICKET-030
568
- - **Description:** A `node`-runnable WS server (`apps/local-cli/src/main.ts`)
569
- that wires `in-memory-runtime` + `ConnectionActor` to a `ws` server,
570
- loads a config file (MOTD, server name, etc.), and logs each connection.
571
- This is the manual-test harness and the e2e fixture target.
572
- - **Acceptance Criteria:**
573
- - `pnpm --filter local-cli start -- --port 6667` boots.
574
- - WeeChat/HexChat can connect, register, join, and talk.
575
- - Logs structured JSON.
576
- - **TDD Outline:** Red on an e2e-ish test spinning up the WS server in a
577
- test process and connecting a tiny client (from `tools/irc-client`
578
- stub); green.
579
-
580
- ### TICKET-032 — Parametrized integration test suite over `IrcRuntime`
581
- - **Phase:** 2
582
- - **Points:** 5
583
- - **Status:** ⬜
584
- - **Dependencies:** TICKET-029, TICKET-030
585
- - **Description:** In `tests/integration`, define IRC scenarios (register,
586
- join, privmsg, mode change, kick, reconnect, nick collision, …) as data,
587
- with a runner that takes any `IrcRuntime` factory. The same suite runs
588
- against `in-memory-runtime` now, against the CF runtime in Phase 3, and
589
- against the AWS runtime in Phase 4.
590
- - **Acceptance Criteria:**
591
- - ≥20 scenarios covering every Phase 1 command + the trickier
592
- cross-authority paths (JOIN mutates both Connection and Channel
593
- state).
594
- - All scenarios green against `in-memory-runtime`.
595
- - Adding a new runtime requires only registering a factory.
596
- - **TDD Outline:** Red on one parametrized scenario; green; then add
597
- scenarios incrementally.
598
-
599
- ---
600
-
601
- ## Phase 3 — Cloudflare adapter
602
-
603
- ### TICKET-033 — `ConnectionDO` (hibernatable WS), state migration, alarms
604
- - **Phase:** 3
605
- - **Points:** 8
606
- - **Status:** ✅
607
- - **Dependencies:** TICKET-030, TICKET-032
608
- - **Description:** Implement `packages/cf-adapter/src/connection-do.ts`
609
- using `DurableObjectState`'s `HibernatingWebSocketHandler`. Owns one
610
- connection's socket + state (via `state.storage`). Routes incoming
611
- frames through `ConnectionActor` with a `CfRuntime` stub. Set alarms
612
- for PING/idle sweeps.
613
- - **Acceptance Criteria:**
614
- - Survives hibernation: state restored on wake.
615
- - WebSocket close releases nick and triggers QUIT fanout via
616
- `ChannelDO` stubs.
617
- - Alarms fire and emit PING; missing PONG closes the connection.
618
- - **TDD Outline:** Red via `@cloudflare/vitest-pool-workers` on
619
- "hibernate then wake preserves nick"; green; then alarm-driven PING.
620
-
621
- ### TICKET-034 — `RegistryDO` with sharding plan
622
- - **Phase:** 3
623
- - **Points:** 5
624
- - **Status:** ✅
625
- - **Dependencies:** TICKET-029
626
- - **Description:** Implement `RegistryDO` keyed by `hash(nick) % N` (N
627
- configurable, default 32). Methods match `reserveNick` / `changeNick` /
628
- `releaseNick`. Single-threaded DO gives the uniqueness invariant for
629
- free.
630
- - **Acceptance Criteria:**
631
- - Two concurrent reservations for the same nick from different
632
- `ConnectionDO`s → exactly one succeeds.
633
- - Sharding function documented and unit-tested.
634
- - **TDD Outline:** Red on concurrent reserve from two stub ConnectionDOs;
635
- green.
636
-
637
- ### TICKET-035 — `ChannelDO` (roster + modes + fanout via ConnectionDO stubs)
638
- - **Phase:** 3
639
- - **Points:** 8
640
- - **Status:** ⬜
641
- - **Dependencies:** TICKET-033, TICKET-034
642
- - **Description:** `ChannelDO` keyed by lowercased channel name. Owns
643
- roster, topic, modes, ban list. Fanout = loop roster, call each member's
644
- `ConnectionDO.fetch()` with the line. Coordinates with `ConnectionDO`
645
- via `state.storage` deltas.
646
- - **Acceptance Criteria:**
647
- - JOIN/PART/KICK/MODE/TOPIC all drive correct state + fanout.
648
- - Gone `ConnectionDO` (websocket closed) removed lazily from roster.
649
- - Up to 1k members in a single channel under the local workerd.
650
- - **TDD Outline:** Red on "two members of a channel both receive
651
- PRIVMSG"; green; then gone-connection sweep.
652
-
653
- ### TICKET-036 — `CfRuntime` implementing `IrcRuntime`
654
- - **Phase:** 3
655
- - **Points:** 5
656
- - **Status:** ⬜
657
- - **Dependencies:** TICKET-029, TICKET-033, TICKET-034, TICKET-035
658
- - **Description:** Wire the three DOs together behind a single
659
- `CfRuntime` class implementing the `IrcRuntime` interface. Each method
660
- issues the correct DO `stub()`/`fetch()` calls.
661
- - **Acceptance Criteria:**
662
- - All `IrcRuntime` methods work end-to-end.
663
- - Cross-DO coordination uses `stub()` (no in-memory caching that could
664
- drift).
665
- - Phase 2 integration suite (TICKET-032) registered against
666
- `CfRuntime` and passing.
667
- - **TDD Outline:** Red on the first integration scenario against
668
- `CfRuntime`; green; iterate.
669
-
670
- ### TICKET-037 — Cloudflare deploy pipeline (wrangler), staging env, smoke e2e
671
- - **Phase:** 3
672
- - **Points:** 3
673
- - **Status:** ⬜
674
- - **Dependencies:** TICKET-036
675
- - **Description:** `apps/cf-worker` deploy glue: `wrangler.toml`,
676
- migrations, secrets. GitHub Actions step to deploy to a `staging`
677
- environment on push to `main`. Smoke e2e test in CI: connect, register,
678
- JOIN, PRIVMSG, QUIT.
679
- - **Acceptance Criteria:**
680
- - `pnpm deploy:cf:staging` works from a clean checkout.
681
- - CI smoke e2e green on every merge.
682
- - **TDD Outline:** Red on a CI job that asserts a deployed server
683
- responds to a CONNECT; green.
684
-
685
- ### TICKET-038 — Docs: `docs/deployment-cf.md`
686
- - **Phase:** 3
687
- - **Points:** 1
688
- - **Status:** ✅
689
- - **Dependencies:** TICKET-037
690
- - **Description:** End-to-end deployment guide: prerequisites, wrangler
691
- config knobs, secrets, sharding, cost notes, troubleshooting.
692
- - **Acceptance Criteria:**
693
- - A new contributor can deploy their own staging instance following
694
- only this doc.
695
- - **TDD Outline:** N/A (documentation). Reviewed by following the steps
696
- from scratch in a clean environment.
697
-
698
- ---
699
-
700
- ## Phase 4 — AWS adapter
701
-
702
- ### TICKET-039 — CDK stack: APIGW WebSocket, Lambda, DynamoDB tables, IAM
703
- - **Phase:** 4
704
- - **Points:** 8
705
- - **Status:** ⬜
706
- - **Dependencies:** TICKET-032
707
- - **Description:** `apps/aws-stack` CDK v2 app. API Gateway v2 WebSocket
708
- API; Node 20 Lambdas for `$connect`/`$disconnect`/`$default`; DynamoDB
709
- tables `Connections`, `ChannelMeta`, `ChannelMembers`, `Nicks`,
710
- `Accounts` per PLAN §4; least-privilege IAM.
711
- - **Acceptance Criteria:**
712
- - `pnpm cdk deploy` brings up the stack in a fresh AWS account.
713
- - `cdk synth` clean, no warnings about broad permissions.
714
- - Local validation via localstack.
715
- - **TDD Outline:** Red on a localstack-backed test asserting all five
716
- tables exist post-deploy; green.
717
-
718
- ### TICKET-040 — `$connect` / `$disconnect` / `$default` handlers + `AwsRuntime`
719
- - **Phase:** 4
720
- - **Points:** 8
721
- - **Status:** ⬜
722
- - **Dependencies:** TICKET-030, TICKET-039
723
- - **Description:** Lambda handlers wiring APIGW events to
724
- `ConnectionActor` with `AwsRuntime`. `AwsRuntime` implements
725
- `IrcRuntime` backed by DynamoDB + `ApiGatewayManagementApi`.
726
- - **Acceptance Criteria:**
727
- - All Phase 2 integration scenarios pass against `AwsRuntime`
728
- (dynamodb-local).
729
- - Send to a gone connection caught and triggers cleanup.
730
- - **TDD Outline:** Red on first integration scenario against
731
- `AwsRuntime`; green; iterate.
732
-
733
- ### TICKET-041 — DynamoDB transactions for membership; gone-connection sweep
734
- - **Phase:** 4
735
- - **Points:** 5
736
- - **Status:** ⬜
737
- - **Dependencies:** TICKET-040
738
- - **Description:** Use `TransactWriteItems` for JOIN/PART/KICK (mutates
739
- `Connections.joined` and `ChannelMembers` atomically). Lazy
740
- gone-connection cleanup on `GoneException` plus a sweeper Lambda on a
741
- schedule.
742
- - **Acceptance Criteria:**
743
- - Concurrent JOIN/PART by the same connection on the same channel
744
- cannot leave inconsistent state (proven by a stress test).
745
- - Sweeper removes stale `Connections`/`ChannelMembers` rows past a TTL.
746
- - **TDD Outline:** Red on concurrent-join race; green; then sweeper
747
- test.
748
-
749
- ### TICKET-042 — EventBridge idle / PING timers
750
- - **Phase:** 4
751
- - **Points:** 3
752
- - **Status:** ⬜
753
- - **Dependencies:** TICKET-040
754
- - **Description:** EventBridge Scheduler one-time per-connection PING
755
- jobs; on PING, Lambda checks last-seen and either posts PONG wait or
756
- disconnects.
757
- - **Acceptance Criteria:**
758
- - Idle connection gets `PING` after the configured interval.
759
- - Missing `PONG` for the configured window → disconnect + cleanup.
760
- - **TDD Outline:** Red on "no PONG for 2× interval → connection
761
- removed"; green.
762
-
763
- ### TICKET-043 — AWS deploy pipeline (CDK), staging env, smoke e2e
764
- - **Phase:** 4
765
- - **Points:** 3
766
- - **Status:** ✅
767
- - **Dependencies:** TICKET-042
768
- - **Description:** GitHub Actions CDK deploy to `staging` on push to
769
- `main`. Smoke e2e identical in shape to TICKET-037.
770
- - **Acceptance Criteria:**
771
- - `pnpm deploy:aws:staging` works from a clean checkout.
772
- - CI smoke e2e green on every merge.
773
- - **TDD Outline:** Red on a CI job asserting a deployed server responds
774
- to CONNECT; green.
775
-
776
- ### TICKET-044 — Docs: `docs/AWS-Deployment.md`
777
- - **Phase:** 4
778
- - **Points:** 1
779
- - **Status:** ✅
780
- - **Dependencies:** TICKET-043
781
- - **Description:** End-to-end AWS deployment guide: prerequisites, CDK
782
- knobs, DynamoDB capacity planning, region strategy, cost notes,
783
- troubleshooting.
784
- - **Acceptance Criteria:**
785
- - A new contributor can deploy their own staging instance following
786
- only this doc.
787
- - **TDD Outline:** N/A (documentation). Reviewed by following the steps
788
- from scratch.
789
-
790
- ### TICKET-066 — CDK `environmentName` parameter + prefixed resource names
791
- - **Phase:** 4
792
- - **Points:** 3
793
- - **Status:** ⬜
794
- - **Dependencies:** TICKET-044 (✅)
795
- - **Description:** The `IrcAwsStack` (`apps/aws-stack/src/aws-stack.ts`)
796
- is a single hard-coded stack: the CloudFormation stack name is
797
- `IrcAwsStack`, every DynamoDB physical table name is the bare logical
798
- id (`Connections`, `ChannelMeta`, …), and the APIGW stage is always
799
- `prod`. Two environments (staging + production) therefore **cannot
800
- coexist in the same AWS account + region** — the second deploy
801
- collides on table names and stack id. This ticket adds an
802
- `environmentName: string` construct prop (e.g. `"staging"`,
803
- `"production"`) that:
804
- 1. Becomes part of the CloudFormation stack id
805
- (`IrcAwsStack-<environmentName>`).
806
- 2. Prefixes every physical DynamoDB table name
807
- (`<env>Connections`, `<env>ChannelMeta`, …) so cross-env table
808
- scans/isolation is unambiguous and the gone-connection sweeper
809
- never touches the wrong env's rows.
810
- 3. Suffixes the APIGW stage name when desirable (or keeps `prod`
811
- per-stage — design call, but the default should remain
812
- client-compatible).
813
- 4. Flows through `bin/aws.ts` from a CDK context variable
814
- (`-c environmentName=staging`) with a default of `staging` for
815
- backward compatibility with the existing deploy pipeline
816
- (TICKET-043).
817
- - **Acceptance Criteria:**
818
- - `cdk synth -c environmentName=staging` and
819
- `cdk synth -c environmentName=production` produce templates whose
820
- DynamoDB table names and stack id differ (no collision).
821
- - `cdk deploy` with no `-c environmentName` defaults to `staging`
822
- (existing pipeline stays green without changes).
823
- - The synth-time test suite (`apps/aws-stack/tests/stack.test.ts`)
824
- gains cases asserting the prefix for at least two env names and the
825
- default-when-omitted path; existing assertions are updated to pass
826
- `environmentName: 'staging'` (or equivalent) in the test harness.
827
- - The Lambda env-var injection loop
828
- (`<NAME>_TABLE`) still injects the **prefixed** physical name so
829
- `buildDepsFromEnv` resolves the correct table at runtime.
830
- - The deploy workflow (`.github/workflows/deploy-aws.yml`) is updated
831
- to pass `-c environmentName=staging` explicitly (so the default
832
- behaviour is intent, not accident) and the smoke URL resolution
833
- step targets `IrcAwsStack-staging`.
834
- - `docs/deployment-aws.md` §7 (Configuration reference) is updated to
835
- document the new knob.
836
- - **TDD Outline:**
837
- 1. Red — synth test: table names contain the env prefix when
838
- `environmentName: 'production'`; currently fails (names are bare).
839
- Green by threading the prop into the `Table` constructs and the
840
- `TABLE_DEFS` spread.
841
- 2. Red — synth test: stack id / CloudFormation stack name includes the
842
- env suffix; green.
843
- 3. Red — synth test: omitting the prop defaults to `staging` and
844
- existing table-name assertions still hold; green by defaulting the
845
- prop in `bin/aws.ts`.
846
- - **Out of scope:**
847
- - Multi-account or multi-region fan-out (region strategy is
848
- documented in `docs/deployment-aws.md` §10; this ticket is
849
- single-account / single-region env isolation only).
850
- - Cross-env data migration (greenfield envs only).
851
-
852
- ### TICKET-067 — Fix MOTD parsing mismatch (CDK default vs config loader)
853
- - **Phase:** 4
854
- - **Points:** 1
855
- - **Status:** ✅
856
- - **Dependencies:** TICKET-043 (✅)
857
- - **Description:** The Lambda config loader
858
- (`packages/aws-adapter/src/config-loader.ts`) splits the `MOTD` env
859
- var on `\n` to produce `motdLines`. But the CDK default injected in
860
- `apps/aws-stack/src/aws-stack.ts` is **comma-separated**:
861
- `'Welcome to the ServerlessIRCd deployment.,This server runs on AWS Lambda.'`
862
- — so a freshly deployed stack advertises a single MOTD line containing
863
- a literal comma instead of two lines. Pick **one** canonical delimiter
864
- and align both sides. `\n` is the existing loader contract and is what
865
- the config schema expects, so the CDK default should switch to a
866
- `\n`-joined string (or the stack should accept a `string[]` and join
867
- it itself, which composes better with TICKET-068).
868
- - **Acceptance Criteria:**
869
- - A freshly deployed stack shows the expected number of MOTD lines
870
- (375 / 372 / 376 sequence with each line on its own numeric), not a
871
- single comma-joined line.
872
- - A unit test in `packages/aws-adapter` asserts that a `\n`-joined
873
- `MOTD` env var round-trips through the loader into the expected
874
- `motdLines` array length.
875
- - The CDK default in `aws-stack.ts` uses `\n` (or the array-join path)
876
- — no comma-separated literals remain.
877
- - `docs/deployment-aws.md` §7 (where the MOTD env var is documented)
878
- is updated to state the delimiter unambiguously.
879
- - **TDD Outline:**
880
- 1. Red — assert that the CDK-injected default MOTD value, when fed
881
- through the loader, yields `motdLines.length >= 2`; currently fails
882
- (length 1). Green by changing the delimiter in the CDK default.
883
- 2. Refactor — if TICKET-068 has landed, the MOTD is sourced from a
884
- `string[]` construct prop and joined with `\n` in the stack, so
885
- the mismatch is structural rather than literal.
886
-
887
- ### TICKET-068 — Make hardcoded stack identity vars configurable via construct props
888
- - **Phase:** 4
889
- - **Points:** 2
890
- - **Status:** ⬜
891
- - **Dependencies:** TICKET-044 (✅)
892
- - **Description:** `apps/aws-stack/src/aws-stack.ts` hard-codes three
893
- deployment-identity values as string literals in `addEnvironment`
894
- calls (around lines 132–145): `SERVER_NAME='irc.example.com'`,
895
- `NETWORK_NAME='ExampleNet'`, and `MOTD='…'`. There is no deploy-time
896
- knob — overriding any of them requires editing source. Promote these
897
- to construct props on `IrcAwsStack` (or a typed `IrcStackProps` bag)
898
- with the current literals as defaults, and flow them through
899
- `bin/aws.ts` from CDK context variables (`-c serverName=…`,
900
- `-c networkName=…`, `-c motd=…`). This composes with TICKET-066
901
- (env-name parameter) — recommend landing 066 first so the prop bag is
902
- extended once rather than twice.
903
- - **Acceptance Criteria:**
904
- - `IrcAwsStack` accepts an optional `{ serverName?, networkName?,
905
- motdLines? }` prop bag; when omitted, the existing defaults apply
906
- (no behaviour change for current deploys).
907
- - `bin/aws.ts` reads `serverName` / `networkName` / `motd` from CDK
908
- context (`app.node.tryGetContext`) and forwards them.
909
- - The same values are injected into **both** the `IrcHandler` and
910
- `IrcPingChecker` Lambdas (both currently carry the hard-coded
911
- copies — de-duplicate via the prop bag).
912
- - Synth-time test: overriding the props produces a template whose
913
- Lambda env vars carry the overridden values.
914
- - `docs/deployment-aws.md` §7.1 (currently documenting these as
915
- "hardcoded, override requires source edit") is updated to show the
916
- `-c` context override path.
917
- - **TDD Outline:**
918
- 1. Red — synth test: passing `serverName: 'irc.my.net'` produces a
919
- Lambda env var `SERVER_NAME=irc.my.net`; currently fails (always
920
- `irc.example.com`). Green by threading the prop into
921
- `addEnvironment`.
922
- 2. Red — synth test: the same override flows to `IrcPingChecker`
923
- (catches the duplicated hard-coded copy); green.
924
- 3. Green — CDK context override path in `bin/aws.ts` end-to-end via
925
- `cdk synth -c serverName=…`.
926
- - **Out of scope:**
927
- - Secrets (server password, SASL creds) — those belong in Secrets
928
- Manager / SSM, not CDK context; tracked separately.
929
-
930
- ---
931
-
932
- ## Phase 5 — Cross-cutting
933
-
934
- ### TICKET-045 — Observability: structured logs, trace IDs, dashboards
935
- - **Phase:** 5
936
- - **Points:** 3
937
- - **Status:** ✅
938
- - **Dependencies:** TICKET-036, TICKET-040
939
- - **Description:** Structured JSON logs everywhere with a per-request
940
- `traceId` propagated via `ctx`. CF Workers Analytics; CloudWatch
941
- dashboards + metrics for connections/sec, msg latency, error rate.
942
- - **Acceptance Criteria:**
943
- - Every log line has `traceId`, `connectionId` where relevant.
944
- - Dashboards checked in (CDK / wrangler config).
945
- - **TDD Outline:** Red on a test asserting log shape via an injected
946
- logger; green.
947
-
948
- ### TICKET-046 — Security: server password, SASL, cloaking, rate limits, caps
949
- - **Phase:** 5
950
- - **Points:** 5
951
- - **Status:** ⬜
952
- - **Dependencies:** TICKET-026, TICKET-028
953
- - **Description:** Production hardening: enforce server password
954
- (configured per deployment), hostmask cloaking for user IPs, per-IP and
955
- per-connection rate limits beyond TICKET-028's flood control, max
956
- channels and connections per user, 512-byte input cap enforced in
957
- parser (verify), max nickname/channel length caps from isupport.
958
- - **Acceptance Criteria:**
959
- - Each limit tested in isolation.
960
- - Documented defaults + override knobs in config.
961
- - **TDD Outline:** Red per limit; green per limit.
962
-
963
- ### TICKET-047 — Config: MOTD, server/network name, channel prefixes, max clients, oper creds
964
- - **Phase:** 5
965
- - **Points:** 2
966
- - **Status:** ✅
967
- - **Dependencies:** TICKET-027
968
- - **Description:** A single `ServerConfig` schema (Zod) loaded from file
969
- per adapter (CF: KV/secret; AWS: Secrets Manager/SSM). Covers MOTD,
970
- server name, network name, allowed channel prefixes, max clients, oper
971
- credentials.
972
- - **Acceptance Criteria:**
973
- - Invalid config fails fast at boot with a readable error.
974
- - Both adapters consume the same schema.
975
- - **TDD Outline:** Red on schema validation of a malformed config;
976
- green.
977
-
978
- ### TICKET-048 — CI hardening: coverage gate ≥90%, contract tests on every PR, mutation testing spot-check
979
- - **Phase:** 5
980
- - **Points:** 3
981
- - **Status:** ✅
982
- - **Dependencies:** TICKET-032
983
- - **Description:** CI workflow enforces ≥90% coverage (100% on
984
- `irc-core`). Integration contract suite runs on every PR.
985
- Stryker/`@stryker-mutator-runner` spot-check on `irc-core/protocol`
986
- and `irc-core/commands` to confirm test quality.
987
- - **Acceptance Criteria:**
988
- - PR with uncovered code fails CI.
989
- - Mutation score ≥80% on spot-checked packages.
990
- - **TDD Outline:** N/A (infra). Validated by intentionally lowering a
991
- test and watching CI fail.
992
-
993
- ---
994
-
995
- ## Phase 6 — Hardening / polish
996
-
997
- ### TICKET-049 — Load test: 10k concurrent connections per platform
998
- - **Phase:** 6
999
- - **Points:** 5
1000
- - **Status:** ⬜
1001
- - **Dependencies:** TICKET-037, TICKET-043
1002
- - **Description:** `tools/load-test` synthetic client farm (k6 or custom)
1003
- that opens N WebSocket connections, registers, joins, and chats.
1004
- Capture p50/p95/p99 latency, drop rate, cost. Produce a written report
1005
- per platform.
1006
- - **Acceptance Criteria:**
1007
- - 10k concurrent connections sustained per platform.
1008
- - Bottlenecks identified and filed as follow-up issues.
1009
- - **TDD Outline:** N/A (perf). Smoke assertion: load test script
1010
- completes without crashing.
1011
-
1012
- ### TICKET-050 — Compatibility sweep: WeeChat, HexChat, IRCCloud, TheLounge, matrix-IRC bridge
1013
- - **Phase:** 6
1014
- - **Points:** 3
1015
- - **Status:** ⬜
1016
- - **Dependencies:** TICKET-031, TICKET-037, TICKET-043
1017
- - **Description:** Manual + scripted compatibility sweep against the
1018
- listed clients. File and fix any incompatibilities found. Record known
1019
- quirks in `docs/compatibility.md`.
1020
- - **Acceptance Criteria:**
1021
- - ≥3 reference clients complete the standard flow without warnings.
1022
- - Quirks documented.
1023
- - **TDD Outline:** N/A (manual/scripted). Output is the compatibility
1024
- matrix.
1025
-
1026
- ### TICKET-051 — ADRs for irreversible decisions
1027
- - **Phase:** 6
1028
- - **Points:** 2
1029
- - **Status:** ⬜
1030
- - **Dependencies:** all prior
1031
- - **Description:** Write ADRs in `docs/adr/` for: state-authority split,
1032
- effect system, DO sharding, DynamoDB schema, wss-only v1, reducer
1033
- purity, dropped TCP support, SASL mechanism scope.
1034
- - **Acceptance Criteria:**
1035
- - Each ADR follows the standard Context/Decision/Consequences format.
1036
- - All irreversible choices from PLAN §9 covered.
1037
- - **TDD Outline:** N/A (documentation).
1038
-
1039
- ---
1040
-
1041
- ## Phase 7 — TLS hardening & raw TCP transport (v1.x)
1042
-
1043
- The v1 plan locked wss-only (PLAN §3, §9). Phase 7 reverses that to add
1044
- irc+tls on :6697 alongside wss, and ships the mTLS work TICKET-026 has
1045
- been waiting on. Raw TCP is a **second adapter per platform**, not a
1046
- config flag — Workers cannot accept raw TCP and API GW is WebSocket-only,
1047
- so both platforms need a different entry point (Cloudflare Spectrum;
1048
- AWS NLB).
1049
-
1050
- ### TICKET-052 — Revise transport decision: dual wss + irc+tls (ADR); update PLAN §3/§9
1051
- - **Phase:** 7
1052
- - **Points:** 2
1053
- - **Status:** ⬜
1054
- - **Dependencies:** TICKET-051
1055
- - **Description:** Write a superseding ADR documenting the dual-transport
1056
- model: wss stays the serverless default; irc+tls :6697 is added for
1057
- RFC-compliant clients. Cover the per-platform mapping (Spectrum on CF,
1058
- NLB on AWS), why wss-only was originally chosen, and what changed the
1059
- decision. Update PLAN §3 ("No plaintext TCP in v1" → "v1.x adds
1060
- irc+tls :6697") and §9 (drop the "wss-only" framing). Supersedes the
1061
- wss-only/dropped-TCP entry in TICKET-051.
1062
- - **Acceptance Criteria:**
1063
- - ADR follows Context/Decision/Consequences and explicitly supersedes
1064
- the wss-only ADR from TICKET-051.
1065
- - PLAN.md §3 and §9 updated; references consistent.
1066
- - **TDD Outline:** N/A (documentation). Reviewed by walking through the
1067
- decision tradeoffs against the original wss-only rationale.
1068
-
1069
- ### TICKET-053 — Generalize `ConnectionActor` transport seam (TCP byte-stream framing)
1070
- - **Phase:** 7
1071
- - **Points:** 3
1072
- - **Status:** ⬜
1073
- - **Dependencies:** TICKET-030
1074
- - **Description:** Extract the `\r\n` line-framing logic from
1075
- `ConnectionActor` (PLAN §9 already tolerates joined WS frames) into a
1076
- `Transport` seam. `ConnectionActor` accepts either a
1077
- `WsTextFrameTransport` (one message per frame, current behavior) or a
1078
- `TcpByteStreamTransport` (stateful `\r\n` framing across chunks with
1079
- partial-line buffering). Parser/reducer/dispatch pipeline is shared
1080
- unchanged — the core is transport-agnostic by design.
1081
- - **Acceptance Criteria:**
1082
- - One message per WS text frame and one message per `\r\n` over a
1083
- chunked TCP stream produce identical reducer invocations.
1084
- - Partial-line buffering across chunks is correct (no message loss or
1085
- duplication at chunk boundaries).
1086
- - 100% coverage on the new framing module.
1087
- - **TDD Outline:** Red on "two TCP chunks split mid-line produce one
1088
- PRIVMSG broadcast"; green; then partial-line, empty-line, and
1089
- mixed `\r\n`/`\n` edge cases.
1090
-
1091
- ### TICKET-054 — mTLS support: edge config + `AccountStore` plumbing (enables SASL EXTERNAL)
1092
- - **Phase:** 7
1093
- - **Points:** 5
1094
- - **Status:** ⬜
1095
- - **Dependencies:** TICKET-026, TICKET-052
1096
- - **Description:** Wire platform-native mTLS into both adapters and feed
1097
- the verified client-cert subject into the `AccountStore` so SASL
1098
- `EXTERNAL` (stubbed `904` in TICKET-026) actually authenticates.
1099
- CF: API Shield mTLS with uploaded CA pool, cert subject exposed to the
1100
- Worker. AWS: API Gateway WebSocket custom-domain mTLS with a PEM trust
1101
- store, subject in the `$connect` event context. Define an
1102
- `MtlsIdentityProvider` port in `irc-core` consumed by `AccountStore`;
1103
- adapters inject the platform implementation.
1104
- - **Acceptance Criteria:**
1105
- - CF and AWS adapters each have a documented mTLS configuration path
1106
- (CA/trust-store upload, custom domain, client-cert verification).
1107
- - SASL `EXTERNAL` succeeds when a valid client cert is presented;
1108
- fails with `904` when absent or untrusted.
1109
- - Identity is plumbed via a port — no cloud deps in `irc-core`.
1110
- - Works over both wss and (once landed) raw TCP+TLS.
1111
- - **TDD Outline:** Red on `AccountStore.verify('EXTERNAL', certSubject)`
1112
- returning the mapped account with a fake provider; green; then
1113
- per-adapter integration tests with a generated test CA.
1114
-
1115
- ### TICKET-055 — CF TCP+TLS adapter via Cloudflare Spectrum + Container origin
1116
- - **Phase:** 7
1117
- - **Points:** 8
1118
- - **Status:** ⬜
1119
- - **Dependencies:** TICKET-053, TICKET-052
1120
- - **Description:** Workers cannot accept raw TCP, so the CF TCP+TLS path
1121
- requires **Cloudflare Spectrum** (TLS termination, Enterprise tier)
1122
- fronting a stateful TCP origin. The origin is a **Cloudflare Container**
1123
- (or external compute) running the IRC core with the generalized TCP
1124
- byte-stream `ConnectionActor` over a persistent runtime. Spectrum
1125
- terminates TLS at the edge and forwards plaintext TCP to the origin.
1126
- Connection state is held in the container runtime and persisted
1127
- per the runtime's storage layer.
1128
- - **Acceptance Criteria:**
1129
- - A client connecting to `irc.example.com:6697` over TLS completes
1130
- registration, JOIN, PRIVMSG, QUIT.
1131
- - TLS is terminated by Spectrum; origin only sees plaintext (unless
1132
- mTLS passthrough is configured via TICKET-054).
1133
- - Connection state survives origin reconnect via the runtime's
1134
- persistence layer.
1135
- - Deploy pipeline (wrangler/terraform for Spectrum + container) is
1136
- checked in.
1137
- - **TDD Outline:** Red on an integration scenario against a local
1138
- TLS-terminating proxy (stunnel) in front of the container; green;
1139
- then e2e against staging Spectrum.
1140
-
1141
- ### TICKET-056 — AWS TCP+TLS adapter via Network Load Balancer (TLS) + Lambda streaming
1142
- - **Phase:** 7
1143
- - **Points:** 8
1144
- - **Status:** ⬜
1145
- - **Dependencies:** TICKET-053, TICKET-052
1146
- - **Description:** NLB with a `tls` listener terminates TLS and invokes a
1147
- **Lambda streaming function** as the target. Connection identity is
1148
- derived from NLB flow metadata (source IP + port) and mapped to the
1149
- existing `connectionId` scheme. Bidirectional traffic flows via the
1150
- Lambda response stream. Reuses `AwsRuntime` (DynamoDB-backed state)
1151
- with the generalized TCP byte-stream `ConnectionActor`. Add CDK
1152
- constructs for NLB + target group + ACM cert; document the NLB+Lambda
1153
- streaming limits (idle timeout, max connection duration).
1154
- - **Acceptance Criteria:**
1155
- - A client connecting to the NLB endpoint on :6697 completes a full
1156
- IRC session end-to-end.
1157
- - Same connection-identity semantics as the APIGW WebSocket path
1158
- (one stable ID per connection, routable via DynamoDB).
1159
- - Bidirectional streaming verified under sustained traffic and on
1160
- reconnect.
1161
- - CDK stack deploys cleanly into a fresh account via localstack.
1162
- - **TDD Outline:** Red on a localstack-backed test simulating NLB→Lambda
1163
- byte framing for a single PRIVMSG; green; then the streaming-back
1164
- path and chunk-boundary cases.
1165
-
1166
- ### TICKET-057 — Extend integration contract suite over transport (wss × TCP+TLS)
1167
- - **Phase:** 7
1168
- - **Points:** 3
1169
- - **Status:** ⬜
1170
- - **Dependencies:** TICKET-032, TICKET-053, TICKET-055, TICKET-056
1171
- - **Description:** Extend the parametrized `IrcRuntime` contract suite
1172
- from TICKET-032 with a `Transport` dimension. Every scenario runs
1173
- against (runtime × transport): in-memory+WS, in-memory+TCP, CF+WS,
1174
- CF+TCP, AWS+WS, AWS+TCP. Add scenarios specific to TCP framing
1175
- (chunk boundaries, slow-loris-style partial sends, `\r\n` only,
1176
- bare `\n` tolerance).
1177
- - **Acceptance Criteria:**
1178
- - All existing scenarios green across both transports on the
1179
- in-memory runtime.
1180
- - TCP-specific framing scenarios (multi-chunk message, partial line,
1181
- mixed line endings) covered with deterministic assertions.
1182
- - Adding a new transport requires only registering a factory.
1183
- - **TDD Outline:** Red on a chunk-boundary scenario against the
1184
- in-memory runtime; green; then port the matrix to CF and AWS.
1185
-
1186
- ### TICKET-058 — Docs: `deployment-cf-tcp.md` and `deployment-aws-tcp.md` (TCP+TLS variants)
1187
- - **Phase:** 7
1188
- - **Points:** 2
1189
- - **Status:** ⬜
1190
- - **Dependencies:** TICKET-055, TICKET-056
1191
- - **Description:** End-to-end deployment guides for the :6697 TCP+TLS
1192
- variants: prerequisites (CF Enterprise for Spectrum; ACM cert for AWS
1193
- NLB), per-platform config knobs, cost/ops implications versus the wss
1194
- path, mTLS trust-store setup (cross-ref TICKET-054), and
1195
- troubleshooting.
1196
- - **Acceptance Criteria:**
1197
- - A new contributor can deploy their own TCP+TLS staging instance on
1198
- each platform following only the doc.
1199
- - Cost and operational caveats (stateful origin on CF, NLB+Lambda
1200
- streaming limits on AWS) are clearly stated.
1201
- - **TDD Outline:** N/A (documentation). Reviewed by following the steps
1202
- from scratch in a clean environment.
1203
-
1204
- ---
1205
-
1206
- ### TICKET-062 — IRCv3 `safelist` capability + `SAFELIST` ISUPPORT token
1207
- - **Phase:** 1
1208
- - **Points:** 1
1209
- - **Status:** ✅
1210
- - **Dependencies:** TICKET-016, TICKET-017, TICKET-027
1211
- - **Description:** Advertise the IRCv3 `safelist` capability and the
1212
- matching `SAFELIST` token in `005 RPL_ISUPPORT`. Per
1213
- <https://ircv3.net/docs/specs/extensions/safelist-3.2.html> this is
1214
- purely a signal to clients that the server will not terminate the
1215
- client connection for issuing `LIST` — the `LIST` reducer itself is
1216
- already non-destructive (TICKET-016), so this ticket is the
1217
- advertisement surface only: a new entry in
1218
- `packages/irc-core/src/caps/capabilities.ts` and a new token in
1219
- `packages/irc-core/src/commands/isupport.ts`.
1220
- - **Acceptance Criteria:**
1221
- - `CAP LS` advertises `safelist` (bare name, no value).
1222
- - `CAP REQ :safelist` is accepted, ACKed, and added to the
1223
- connection's negotiated cap set; `CAP LIST` then includes it.
1224
- - `005 RPL_ISUPPORT` advertises the bare `SAFELIST` token on every
1225
- line set (independent of cap negotiation — ISUPPORT is global).
1226
- - `LIST` still works identically before and after negotiating
1227
- `safelist` (no behaviour change — the cap is purely advisory).
1228
- - **TDD Outline:** Red on `isSupportedCap('safelist')` and on a missing
1229
- `SAFELIST` token in the 005 output; green by adding the table entry
1230
- and the token; then assert end-to-end via `CAP REQ` + `CAP LIST` and
1231
- confirm `LIST` still produces `321`/`322`/`323`.
1232
-
1233
- ---
1234
-
1235
- ### TICKET-063 — IRCv3 `draft/chathistory` playback (bouncer-style backlog on JOIN + explicit query)
1236
- - **Phase:** 1
1237
- - **Points:** 8
1238
- - **Status:** ✅
1239
- - **Dependencies:** TICKET-005 (JOIN ✅), TICKET-007 (PRIVMSG/NOTICE ✅),
1240
- TICKET-017 (CAP ✅), TICKET-018 (`message-tags` + `server-time` ✅),
1241
- TICKET-025 (`batch` ✅)
1242
- - **Description:** Implement the IRCv3 `draft/chathistory` extension
1243
- (<https://ircv3.net/specs/extensions/chathistory>) so a reconnecting
1244
- client receives the messages it missed — the bouncer-style behaviour
1245
- users expect from ZNC / TheLounge. Two surfaces ship here:
1246
-
1247
- 1. **Explicit query** — a `CHATHISTORY` command with the spec subcommands
1248
- `BEFORE`, `AFTER`, `LATEST`, `AROUND`, `BETWEEN`, `TARGETS`. The
1249
- server replies with a `BATCH` of type `chathistory` containing the
1250
- matching `PRIVMSG`/`NOTICE`/`TAGMSG` lines, each tagged with
1251
- `@time=…` (server-time) and `msgid=…` (message-tags).
1252
- 2. **Auto-playback on JOIN** — when a client with the cap negotiated
1253
- joins a channel, the server prepends a `BATCH chathistory` of the
1254
- last N messages (default 50, configurable in `ServerConfig`) to the
1255
- normal JOIN/353/366 sequence. A per-`(connection, channel)` "last
1256
- read" marker is kept in `ConnectionState` so repeated JOIN/PART
1257
- cycles do not re-feed the same backlog; the marker advances to the
1258
- newest replayed msgid on every playback.
1259
-
1260
- The persistence layer is behind a new `MessageStore` port
1261
- (`packages/irc-core/src/ports.ts`), mirroring the `MotdProvider` /
1262
- `AccountStore` pattern: the port is **synchronous** for record
1263
- (reducers must stay pure) and **sync-returning-via-effect** for query
1264
- (the reducer emits a new `QueryHistory` effect that `dispatch`
1265
- resolves against `IrcRuntime.queryHistory`, exactly as `LookupNick`
1266
- and `GetConnectionInfo` are resolved today). Recording is driven by a
1267
- new `RecordMessage` effect emitted alongside `Broadcast` from the
1268
- channel PRIVMSG/NOTICE reducer; the dispatch layer fans the record
1269
- into the bound `MessageStore`. Concrete adapters (in-memory ring
1270
- buffer for tests/local-cli, D1/R2 for CF, DynamoDB for AWS) ship in
1271
- follow-up Phase 2/3/4 tickets — this ticket defines the contract and
1272
- the in-memory reference implementation only.
1273
-
1274
- Cap advertisement: `draft/chathistory` (bare name, no value) in
1275
- `capabilities.ts`. The cap MUST be negotiated before any
1276
- `CHATHISTORY` command is accepted and before JOIN auto-playback is
1277
- emitted; clients without the cap see no change whatsoever (RFC 2812
1278
- behaviour).
1279
- - **Acceptance Criteria:**
1280
- - `CAP LS` advertises `draft/chathistory` (bare name); `CAP REQ
1281
- :draft/chathistory` is ACKed and recorded on the connection.
1282
- - `CHATHISTORY` is `421 ERR_UNKNOWNCOMMAND` (or silently ignored per
1283
- spec) when the cap is not negotiated.
1284
- - `CHATHISTORY LATEST #foo * 50` returns up to 50 most-recent
1285
- messages wrapped in `BATCH +id chathistory #foo … BATCH -id`,
1286
- oldest first, each line carrying `@time=…;msgid=…` tags.
1287
- - `CHATHISTORY BEFORE #foo <msgid> 50` / `AFTER #foo <msgid> 50` /
1288
- `BETWEEN #foo <msgid1> <msgid2>` / `AROUND #foo <msgid> 10` return
1289
- the correct slices; bounds (start/end of history) return fewer
1290
- than the requested limit with no error.
1291
- - `CHATHISTORY TARGETS * *` enumerates channels with recent activity
1292
- (used by clients to discover what to fetch after a disconnect).
1293
- - Unknown msgid reference → `ERR_INVALIDPARAMS (461)` per spec;
1294
- non-existent channel → `403`; requester not on `+s`/`+p` channel
1295
- they are not a member of → empty batch (no leak).
1296
- - JOIN by a cap-enabled client emits the auto-playback BATCH **before**
1297
- the `JOIN`/`353`/`366` lines, then advances the per-connection
1298
- last-read marker for that channel to the newest replayed msgid.
1299
- - PART + re-JOIN with no new messages in between replays **nothing**
1300
- (marker is unchanged); PART + re-JOIN after K new messages replays
1301
- exactly those K.
1302
- - A `RecordMessage` effect is emitted for every accepted channel
1303
- PRIVMSG and NOTICE (NOT for NOTICE-to-user or for messages blocked
1304
- by `+n`/`+m`/`+b`); the dispatch layer records it via
1305
- `runtime.recordMessage(...)` with a fresh msgid from
1306
- `IdFactory.nonce()` and `ctx.clock.now()` as the timestamp.
1307
- - The `MessageStore` port is defined in `ports.ts`; an
1308
- `InMemoryMessageStore` (ring buffer, configurable max per channel)
1309
- ships as the reference implementation, used by the local CLI and
1310
- tests.
1311
- - 100% coverage on the new reducer, the port, the in-memory store,
1312
- and the JOIN/PRIVMSG hooks.
1313
- - **TDD Outline:**
1314
- 1. **Red** — `isSupportedCap('draft/chathistory')` returns true; cap
1315
- table test fails.
1316
- 2. **Green** — add the cap entry; assert `CAP LS` / `CAP REQ` /
1317
- `CAP LIST` round-trip.
1318
- 3. **Red** — `InMemoryMessageStore.record(...)` then `query({chan,
1319
- direction:'latest', limit:50})` returns the recorded messages in
1320
- chronological order; fails (no port).
1321
- 4. **Green** — implement the ring buffer with msgid/time indices.
1322
- 5. **Red** — channel PRIVMSG reducer emits a `RecordMessage` effect
1323
- alongside `Broadcast`; fails (no such effect tag).
1324
- 6. **Green** — extend the `Effect` union with `RecordMessage`; thread
1325
- the msgid (`ctx.ids.nonce()`) and time (`ctx.clock.now()`) through
1326
- the broadcast line so the recorded payload matches what peers see.
1327
- 7. **Red** — `chathistoryReducer` on `CHATHISTORY LATEST #foo * 2`
1328
- emits a `QueryHistory` effect whose resolved reply is the BATCH
1329
- frame; fails (no reducer).
1330
- 8. **Green** — implement the reducer; cover each subcommand and each
1331
- error path as its own test.
1332
- 9. **Red** — `joinReducer` with a cap-enabled joiner and a non-empty
1333
- `MessageStore` prepends a `chathistory` BATCH and advances the
1334
- marker; fails.
1335
- 10. **Green** — wire the JOIN hook; cover the empty-store case, the
1336
- PART/re-JOIN no-op case, and the marker-advance case.
1337
- 11. **Refactor** — extract the BATCH-wrapping into a shared helper
1338
- alongside `wrapBatch` (TICKET-025) so the chathistory BATCH and
1339
- the NAMES BATCH share framing code.
1340
- - **Out of scope (follow-up tickets):**
1341
- - Phase 2 — wire the `MessageStore` into `ConnectionActor` +
1342
- `IrcRuntime`; ship a persistent variant in `in-memory-runtime`.
1343
- - Phase 3 — `R2`/`D1`-backed `MessageStore` for the CF adapter
1344
- (`MessageStoreDO` keyed by lowercased channel, lifecycle-managed by
1345
- `ChannelDO`). Retention/TTL policy goes here.
1346
- - Phase 4 — DynamoDB-backed `MessageStore` for the AWS adapter
1347
- (`Messages` table, partition key `chan`, sort key `time`; GSI on
1348
- `msgid` for BEFORE/AFTER pivots).
1349
- - Per-user `+H` channel mode (max history entries) and
1350
- `draft/pre-away` markers — these layer on top of this ticket's
1351
- marker plumbing.
1352
-
1353
- ---
1354
-
1355
- ### TICKET-064 — Flood control: per-command cost + control-plane exemption
1356
- - **Phase:** 1
1357
- - **Points:** 2
1358
- - **Status:** ⬜
1359
- - **Dependencies:** TICKET-028 (✅)
1360
- - **Description:** The flood-control wrapper landed in TICKET-028
1361
- charges a flat `MESSAGE_COST = 1` for every observed command. That is
1362
- too blunt: a client exchanging `PING`/`PONG` with the server, or
1363
- negotiating `CAP` / `AUTHENTICATE` during SASL, can flood itself out
1364
- through no fault of its own — mainstream ircds (charybdis, hybrid,
1365
- Unreal) exempt exactly these control-plane commands for that reason.
1366
- This ticket generalizes `FloodControlConfig` so the cost is a pure
1367
- function of the command name, ships a sensible default exemption set,
1368
- and keeps backward compatibility with the v1 flat-cost behaviour.
1369
- - **Acceptance Criteria:**
1370
- - `FloodControlConfig` gains an optional `commandCost?: (command:
1371
- string) => number` field. When omitted, the wrapper uses the
1372
- existing flat cost (`1`) for every command — i.e. TICKET-028
1373
- callers see no behaviour change.
1374
- - The wrapper exports a ready-made `defaultCommandCost` function (or
1375
- equivalent named export) that returns `0` for the exempt set
1376
- `PING`, `PONG`, `CAP`, `AUTHENTICATE`, `QUIT` and `1` for
1377
- everything else. The set is documented inline and overridable by
1378
- callers composing their own cost function.
1379
- - A command with cost `0` does NOT decrement the bucket and does NOT
1380
- trigger the disconnect path even when the bucket is exhausted —
1381
- the inner reducer is called as normal.
1382
- - Negative costs are rejected at wrapper-construction time (throw /
1383
- assert) — they would invert the bucket.
1384
- - Cost is derived from the parsed `IrcMessage.command` (uppercased
1385
- by the parser), so case-insensitivity is handled once.
1386
- - Determinism preserved: same `(Clock, message stream)` → same
1387
- decision, regardless of cost function (the cost function itself
1388
- is pure).
1389
- - 100% coverage on the new code paths in `flood-control.ts`,
1390
- including the rejection path and the `commandCost === undefined`
1391
- fallback.
1392
- - **TDD Outline:**
1393
- 1. Red — `PING` arriving when the bucket is at `0` tokens still
1394
- delegates to the inner reducer (currently fails: wrapper
1395
- disconnects). Green by adding the cost seam and the default
1396
- exempt set.
1397
- 2. Red — passing a `commandCost` that returns `0` for `CAP` and
1398
- asserting a `CAP LS` burst (10 frames in 1ms with capacity 5)
1399
- does NOT disconnect; green.
1400
- 3. Red — `commandCost: () => -1` throws at
1401
- `wrapWithFloodControl(...)` time; green by validating.
1402
- 4. Red — when `commandCost` is omitted, behaviour matches
1403
- TICKET-028 exactly (regression test on the existing 11-test
1404
- suite; all still pass unchanged).
1405
- 5. Refactor — extract the cost decision into a tiny pure helper so
1406
- the disconnect branch and the pass-through branch share one
1407
- "new balance" computation.
1408
-
1409
- ### TICKET-065 — Wire `wrapWithFloodControl` into the `ConnectionActor`
1410
- - **Phase:** 2
1411
- - **Points:** 3
1412
- - **Status:** ⬜
1413
- - **Dependencies:** TICKET-028 (✅), TICKET-030 (✅)
1414
- - **Description:** The flood-control wrapper (TICKET-028) and its
1415
- per-command cost refinement (TICKET-064, recommended-first) exist but
1416
- are not applied anywhere — a client can still send an unbounded
1417
- command stream to a `ConnectionActor` and the actor will route every
1418
- frame. This ticket wires the wrapper into the actor pipeline so a
1419
- flooding connection is actually disconnected end-to-end, with the
1420
- thresholds sourced from `ServerConfig` so deployments can tune or
1421
- disable flood control without code changes. Mirrors the shape of the
1422
- other "wire reducer X into the actor" tickets (TICKET-059 / -060 /
1423
- -061).
1424
- - **Acceptance Criteria:**
1425
- - `ServerConfig` gains an optional `floodControl?: FloodControlConfig
1426
- & { commandCost?: ... }` field (shape delegated to TICKET-064).
1427
- When omitted, the actor applies a documented default
1428
- (`{ capacity, refillRatePerSecond, disconnectThreshold }` matching
1429
- the charybdis-class defaults cited in the flood-control tests);
1430
- when set to `null` explicitly, flood control is disabled for that
1431
- deployment.
1432
- - The actor wraps every reducer it dispatches (connection-authority
1433
- AND channel-authority routes) so flooding is bounded regardless of
1434
- target — flood control is about bytes-in from the client, not
1435
- about which state the command mutates.
1436
- - The wrapped reducer receives a `ctx.clock` consistent with the
1437
- actor's existing `Clock` injection (no new time source).
1438
- - An actor-level test: a client sending `capacity + threshold + 1`
1439
- `PRIVMSG` frames at fake-clock `t=0` is disconnected with a
1440
- `Disconnect("Excess Flood")` effect that the actor dispatches to
1441
- the bound `IrcRuntime`, and the underlying socket/close path is
1442
- invoked (asserted via a fake runtime + close spy).
1443
- - A second actor-level test: `PING`/`CAP` frames interleaved into the
1444
- burst do NOT advance the bucket (proves TICKET-064 is plumbed
1445
- through) — gated on TICKET-064 landing first; if 064 has not
1446
- landed, this test is skipped or adapted to the flat-cost model.
1447
- - The `apps/local-cli` server boots with the default config and a
1448
- manual flood (e.g. `yes 'PRIVMSG #foo :bar' | nc`) disconnects the
1449
- client within the configured threshold.
1450
- - Existing actor tests stay green; the wrapper is additive.
1451
- - **TDD Outline:**
1452
- 1. Red — actor test: 20 `PRIVMSG` frames in one tick against the
1453
- default-capacity bucket emit a `Disconnect` to the fake runtime;
1454
- currently fails (no wiring). Green by wrapping the reducer map at
1455
- actor construction.
1456
- 2. Red — actor test: `ServerConfig.floodControl = null` disables the
1457
- wrapper and the same 20-frame burst passes through; green by
1458
- gating wrapper construction on the config field.
1459
- 3. Red — actor test: a `PING` interleaved into the burst does not
1460
- change the disconnect count; green by threading
1461
- `commandCost`/TICKET-064's default exempt set into the wired
1462
- config.
1463
- 4. Refactor — extract reducer-map construction into a pure
1464
- `buildRoutedReducers(config)` helper so the actor and tests can
1465
- inspect the wrapped map without spinning up a full actor.
1466
- 5. Manual smoke against `apps/local-cli` (documented in the ticket
1467
- PR, not automated) confirming a real client gets dropped on flood.
1468
- - **Out of scope (follow-ups):**
1469
- - Per-IP / per-listener rate limits beyond the per-connection bucket
1470
- (TICKET-046 territory).
1471
- - Queue-based policy (still needs a `Buffer` effect — separate
1472
- ticket).
1473
- - Observability metrics (disconnects/sec) — TICKET-045.
1474
-
1475
- ---
1476
-
1477
- ### TICKET-069 — Wire `CHATHISTORY` into the `ConnectionActor` dispatcher + `MessageStore` plumbing
1478
- - **Phase:** 1
1479
- - **Points:** 3
1480
- - **Status:** ✅
1481
- - **Dependencies:** TICKET-063 (chathistory reducer + `MessageStore`, ✅),
1482
- TICKET-030 (actor, ✅), TICKET-059/060/061 (actor wiring pattern, ✅)
1483
- - **Description:** The pure `chathistoryReducer`
1484
- (`packages/irc-core/src/commands/chathistory.ts`) is fully implemented
1485
- and tested (TICKET-063 ✅), but `ConnectionActor.route()`
1486
- (`packages/irc-server/src/actor.ts`) has no `case 'CHATHISTORY'`, so the
1487
- command falls through to the `default` branch → `421
1488
- ERR_UNKNOWNCOMMAND`. Additionally the actor's `buildCtx({...})` call
1489
- does not thread a `MessageStore`, so `ctx.messages` is always
1490
- `undefined`: chathistory has no data to read, and the PRIVMSG/NOTICE
1491
- recording path (`ctx.messages.record(...)` in the channel reducer) is
1492
- inert. This ticket is the wiring layer (the mirror of TICKET-059/060/061
1493
- which wired WHOIS/AUTHENTICATE/AWAY into the actor) — the reducer itself
1494
- is NOT modified.
1495
-
1496
- Two changes:
1497
- 1. **`MessageStore` plumbing** — add an optional `messages?` field to
1498
- `ConnectionActorOptions`, store it on the actor, and pass it to
1499
- `buildCtx({...})` so `ctx.messages` is defined when a store is bound
1500
- (and stays `undefined` when none is bound, preserving no-store
1501
- behaviour for existing callers/tests). This lights up the
1502
- synchronous recording path in the PRIVMSG/NOTICE channel reducer
1503
- (the data source for JOIN auto-playback) without touching `dispatch`
1504
- or `IrcRuntime` (recording is synchronous, no new `Effect`).
1505
- 2. **Dispatcher routing** — add `case 'CHATHISTORY'` that calls
1506
- `chathistoryReducer(channel, msg, ctx).effects` directly (the same
1507
- bespoke-signature pattern as `routeList`/`routeWho`/`routeWhois`; NOT
1508
- added to `buildRoutedReducers`). The target channel is
1509
- `msg.params[1]` (params[0] is the subcommand). It must be resolved
1510
- WITHOUT creating it as a side effect: add an optional `getChannel?`
1511
- peek to `ActorChannelAccess` (mirrors `refreshChannel?`) and have the
1512
- in-memory runtimes implement it; pass `undefined` when the channel
1513
- does not exist (reducer emits `403`).
1514
- - **Acceptance Criteria:**
1515
- - `actor.ts` contains a `case 'CHATHISTORY'`.
1516
- - A connection that negotiated `draft/chathistory` (plus `server-time`,
1517
- `message-tags`, `batch`), with a bound `InMemoryMessageStore` seeded
1518
- with messages in `#foo`, receiving `CHATHISTORY LATEST #foo * 2` emits
1519
- a `BATCH +… chathistory` frame (NOT `421`).
1520
- - A connection WITHOUT the cap receiving `CHATHISTORY ...` still gets
1521
- `421` (routing reaches the reducer, which does cap-gating internally).
1522
- - An unknown channel target emits `403 ERR_NOSUCHCHANNEL` via the reducer.
1523
- - `ctx.messages` is `undefined` when no store is bound; existing actor
1524
- tests pass unchanged (no crash, PRIVMSG does not record).
1525
- - A channel PRIVMSG through the actor with a bound store records into
1526
- the store (the JOIN auto-playback data source).
1527
- - `ActorChannelAccess` gains an optional `getChannel?` peek; resolving a
1528
- CHATHISTORY target never creates a channel.
1529
- - `packages/irc-core` chathistory tests remain green UNCHANGED (reducer
1530
- behaviour is not modified).
1531
- - **TDD Outline:**
1532
- 1. **Red #1** — actor test: bound store + negotiated caps + seeded
1533
- messages + `CHATHISTORY LATEST #foo * 2` → expects a `BATCH
1534
- chathistory` frame, not `421`. Currently fails (421 fallthrough).
1535
- 2. **Red #2** — actor test: no cap + `CHATHISTORY ...` → still `421`
1536
- (regression: routing reaches the reducer which cap-gates).
1537
- 3. **Green** — add `case 'CHATHISTORY'`; thread `messages` into
1538
- `buildCtx`; add `getChannel?` peek + implement in `FakeRuntime`; cover
1539
- the unknown-target (→ `403`) path.
1540
- 4. **Refactor** — confirm no-store callers behave unchanged; assert
1541
- PRIVMSG recording lights up when a store is bound.
1542
-
1543
- ---
1544
-
1545
- ## Phase 8 — PLAN-FIXES Remediation
1546
-
1547
- Tickets derived from `PLAN-FIXES.md` (the post-Phase-1–7 audit of stubbed,
1548
- incomplete, and not-fully-wired functionality). Each ticket's **Phase:**
1549
- field records the logical phase the work conceptually belongs to (the
1550
- section it remediates); the tickets are collected here as a single
1551
- remediation batch for clean tracking. Grouped by PLAN-FIXES severity:
1552
- **Critical** (TICKET-070–074) → **Absent IRC commands** (TICKET-075–078) →
1553
- **Partial** (TICKET-079–085) → **Minor / Dead scaffolding**
1554
- (TICKET-086–092).
1555
-
1556
- ---
1557
-
1558
- ### TICKET-070 — Wire `MessageStore` into all three adapters (chathistory end-to-end)
1559
- - **Phase:** 8 (remediates Phase 3/4 wiring; cross-cutting)
1560
- - **Points:** 5
1561
- - **Status:** ⬜
1562
- - **Dependencies:** TICKET-069 (actor `MessageStore` plumbing, ✅),
1563
- TICKET-063 (chathistory reducer + `MessageStore` port, ✅)
1564
- - **Description:** PLAN-FIXES #1. `ConnectionActor` accepts
1565
- `messages?: MessageStore` (`packages/irc-server/src/actor.ts:105`)
1566
- since TICKET-069, and `irc-core` ships a complete
1567
- `InMemoryMessageStore`, the `CHATHISTORY` reducer, and JOIN
1568
- auto-playback. None of the three adapters construct and pass a
1569
- `MessageStore` at the actor-construction sites:
1570
-
1571
- - `packages/cf-adapter/src/connection-do.ts:303-312`
1572
- - `packages/aws-adapter/src/handlers/default.ts:103-111`
1573
- - `apps/local-cli/src/server.ts:480-501`
1574
-
1575
- **Impact:** chathistory recording and playback are dead code outside
1576
- unit tests; `ctx.messages` is always `undefined` in any deployment, so
1577
- the `draft/chathistory` cap (TICKET-063) is advertised but never
1578
- functional end-to-end.
1579
- - **Acceptance Criteria:**
1580
- - Each adapter constructs a `MessageStore` instance and passes it via
1581
- `messages: ...` in `ConnectionActorOptions`.
1582
- - `apps/local-cli`: `InMemoryMessageStore` (per-process ring buffer,
1583
- config-driven max-per-channel).
1584
- - CF adapter: a `MessageStore` scoped to the connection (in-worker
1585
- `InMemoryMessageStore` as the minimum viable impl; a
1586
- Durable-Object-backed persistent variant is a documented follow-up).
1587
- - AWS adapter: a DynamoDB-backed `MessageStore` (new `Messages` table,
1588
- partition key `chan`, sort key `time`, GSI on `msgid` for
1589
- BEFORE/AFTER pivots) OR an in-memory v1 impl with persistence as a
1590
- documented follow-up.
1591
- - End-to-end per adapter: a client negotiating `draft/chathistory` +
1592
- `server-time` + `message-tags` + `batch`, sending a PRIVMSG to a
1593
- channel, then issuing `CHATHISTORY LATEST #chan * 10`, receives the
1594
- recorded message back in a `BATCH +… chathistory` frame.
1595
- - JOIN auto-playback prepends a BATCH on rejoin after messages were
1596
- sent while the client was away.
1597
- - 100% coverage on the new adapter wiring code.
1598
- - **TDD Outline:**
1599
- 1. Red — per adapter, an integration test asserting
1600
- `CHATHISTORY LATEST` returns the just-recorded PRIVMSG; currently
1601
- fails (store not bound → empty result).
1602
- 2. Green — construct and bind the store at each actor-construction
1603
- site.
1604
- 3. Refactor — extract a shared `bindMessageStore(env/config)` helper
1605
- per adapter so the construction logic is unit-testable in isolation.
1606
- - **Out of scope:** retention/TTL policies on the persistent stores
1607
- (follow-up once the storage shape lands).
1608
-
1609
- ---
1610
-
1611
- ### TICKET-071 — Wire `AccountStore` into the `ConnectionActor` (SASL PLAIN end-to-end)
1612
- - **Phase:** 8 (remediates Phase 3/4 wiring; cross-cutting)
1613
- - **Points:** 8
1614
- - **Status:** ⬜
1615
- - **Dependencies:** TICKET-026 (SASL reducer, ✅),
1616
- TICKET-060 (AUTHENTICATE actor wiring, ✅)
1617
- - **Description:** PLAN-FIXES #2. `ConnectionActorOptions`
1618
- (`packages/irc-server/src/actor.ts:89-119`) has **no `accounts`**
1619
- field, and `route()` → `buildCtx()`
1620
- (`packages/irc-server/src/actor.ts:238-245`) never supplies one, even
1621
- though `buildCtx` supports it
1622
- (`packages/irc-core/src/types.ts:143`).
1623
-
1624
- **Impact:** SASL PLAIN authentication can never succeed in any
1625
- adapter. A client completing `AUTHENTICATE PLAIN` always receives
1626
- `904 ERR_SASLFAIL` because `ctx.accounts` is `undefined`. Only unit
1627
- tests (which call `buildCtx` directly) exercise the happy path.
1628
- - **Acceptance Criteria:**
1629
- - `ConnectionActorOptions` gains an optional `accounts?: AccountStore`
1630
- field; the actor stores it and forwards it through `route()` into
1631
- `buildCtx`.
1632
- - When no store is bound, `ctx.accounts` is `undefined` (existing
1633
- behaviour preserved for callers/tests that do not opt in).
1634
- - Each adapter supplies an `AccountStore`:
1635
- - CF: an in-worker `AccountStore` seeded from config as the minimum
1636
- viable impl; a Durable-Object / D1-backed persistent variant is a
1637
- follow-up.
1638
- - AWS: a DynamoDB-backed `AccountStore` against the `Accounts` table
1639
- (cross-ref TICKET-072).
1640
- - local-cli: an in-memory `AccountStore` seeded from config.
1641
- - End-to-end per adapter: a client negotiating `sasl` and sending
1642
- `AUTHENTICATE PLAIN` with valid creds gets `900`/`903`; invalid creds
1643
- get `904`; the account name is recorded on the connection for
1644
- `extended-join`/`account-tag`.
1645
- - 100% coverage on the actor plumbing and on each adapter's
1646
- `AccountStore`.
1647
- - **TDD Outline:**
1648
- 1. Red — actor test: bound `AccountStore` + `AUTHENTICATE PLAIN` with
1649
- valid creds returns `903`; currently fails (always `904`).
1650
- 2. Red — actor test: no store bound + `AUTHENTICATE PLAIN` returns
1651
- `904` (regression guard preserving existing behaviour).
1652
- 3. Green — add `accounts?` to options; thread into `buildCtx`.
1653
- 4. Red — per adapter: end-to-end `AUTHENTICATE PLAIN` with a seeded
1654
- account.
1655
- 5. Green — implement each adapter's `AccountStore`.
1656
- 6. Refactor — share credential-resolution logic across adapters via a
1657
- helper.
1658
-
1659
- ---
1660
-
1661
- ### TICKET-072 — Decide fate of AWS `Accounts` DynamoDB table (implement AccountStore or remove)
1662
- - **Phase:** 8 (remediates Phase 4)
1663
- - **Points:** 8
1664
- - **Status:** ⬜
1665
- - **Dependencies:** TICKET-039 (CDK stack, ✅),
1666
- TICKET-071 (AccountStore plumbing)
1667
- - **Description:** PLAN-FIXES #5. The `Accounts` DynamoDB table is
1668
- provisioned in CDK (`packages/aws-adapter/src/cdk-table-defs.ts:61-65`)
1669
- and required by `tablesConfigFromNames`, but no runtime code reads or
1670
- writes it. No `AccountStore` implementation is backed by it.
1671
-
1672
- **Impact:** paid-for infrastructure with no consumer; also blocks SASL
1673
- account persistence (see TICKET-071).
1674
- - **Acceptance Criteria:**
1675
- - Decision documented in an ADR or in `docs/AWS-Deployment.md`:
1676
- either (a) implement a DynamoDB-backed `AccountStore`, or (b) remove
1677
- the table until it is needed.
1678
- - If implementing: a `DynamoAccountStore` class backed by the
1679
- `Accounts` table; `verify('PLAIN', payload)` queries by username,
1680
- validates credentials (hashed — never plaintext), returns the
1681
- account. CRUD/seed tooling for adding oper/SASL accounts documented.
1682
- - If removing: `cdk-table-defs.ts` no longer provisions `Accounts`;
1683
- `tablesConfigFromNames` no longer requires it; deploy notes updated;
1684
- TICKET-071's AWS adapter uses an in-memory store seeded from Secrets
1685
- Manager / SSM until persistence is needed.
1686
- - `cdk diff` reflects the chosen path; existing synth tests updated.
1687
- - 100% coverage on whichever code path ships.
1688
- - **TDD Outline:**
1689
- 1. Decision spike — implement-vs-remove; document in the ticket PR.
1690
- 2. Red — synth/integration test asserting the chosen end state (table
1691
- present + `AccountStore` implemented, OR table absent); currently
1692
- fails (table present, no consumer).
1693
- 3. Green — implement the chosen path.
1694
- 4. Refactor — extract the `AccountStore`-vs-table decision into config
1695
- so operators can swap implementations.
1696
-
1697
- ---
1698
-
1699
- ### TICKET-073 — Cloudflare runtime: implement stubbed `getConnection`/`getChannelConnections`/`listChannels`/cross-connection `disconnect`
1700
- - **Phase:** 8 (remediates Phase 3)
1701
- - **Points:** 13
1702
- - **Status:** ⬜
1703
- - **Dependencies:** TICKET-036 (CfRuntime, ✅),
1704
- TICKET-033 (ConnectionDO, ✅), TICKET-035 (ChannelDO, ✅)
1705
- - **Description:** PLAN-FIXES #3. The AWS runtime fully implements all
1706
- 16 `IrcRuntime` methods against DynamoDB. The CF runtime
1707
- (`packages/cf-adapter/src/cf-runtime.ts`) has explicit stubs that
1708
- break three commands plus remote disconnect for anything beyond the
1709
- local connection:
1710
-
1711
- | Method | Line | Stub | Impact |
1712
- |---|---|---|---|
1713
- | `getConnection()` | cf-runtime.ts:234-244 | returns `null` for remote conns | Remote WHOIS broken (only `401` + `318`) |
1714
- | `getChannelConnections()` | cf-runtime.ts:246-251 | returns empty `Map` | WHO broken on CF |
1715
- | `listChannels()` | cf-runtime.ts:253-258 | returns `[]` | LIST returns no channels on CF |
1716
- | `disconnect()` | cf-runtime.ts:115-126 | cross-connection disconnect is a no-op | KICK of a remote user does not close their socket |
1717
-
1718
- **Fix:** add the missing ConnectionDO RPCs (`getConnState`,
1719
- channel-membership enumeration, a channel registry) and a
1720
- cross-connection `disconnect` RPC; wire `cf-runtime.ts` to call them.
1721
- - **Acceptance Criteria:**
1722
- - New ConnectionDO RPCs: `getConnState()` returns the connection's
1723
- authoritative `ConnectionState`; cross-connection
1724
- `disconnect(reason)` closes the target DO's WebSocket.
1725
- - New channel-membership enumeration RPC on `ChannelDO` so
1726
- `getChannelConnections(chan)` can resolve every member to a
1727
- `ConnectionState` via the new `getConnState` RPC.
1728
- - Channel registry for `listChannels()`: either a dedicated
1729
- `ChannelRegistryDO` or a KV-backed index updated on channel
1730
- create/destroy.
1731
- - `cf-runtime.ts`:
1732
- - `getConnection(remoteConn)` → calls the target ConnectionDO's
1733
- `getConnState` RPC; returns `null` on missing/gone.
1734
- - `getChannelConnections(chan)` → enumerates ChannelDO members and
1735
- resolves each to a `ConnectionState`.
1736
- - `listChannels()` → queries the registry; returns `ChanSnapshot[]`.
1737
- - `disconnect(remoteConn, reason)` → calls the target ConnectionDO's
1738
- `disconnect` RPC; no-op on missing.
1739
- - WHOIS of a remote user on CF returns the full
1740
- `311`/`319`/`312`/`317`/`318` sequence (not just `401`+`318`).
1741
- - WHO on CF returns channel members.
1742
- - LIST on CF returns channels.
1743
- - KICK of a remote user closes their WebSocket on CF.
1744
- - Phase 2 integration suite (TICKET-032) green against the
1745
- now-complete `CfRuntime`.
1746
- - 100% coverage on the new RPCs and the cf-runtime.ts methods.
1747
- - **TDD Outline:**
1748
- 1. Red — unit test: `cfRuntime.getConnection(remoteConnId)` returns a
1749
- non-null `ConnectionState` when the stub returns one; currently
1750
- fails (returns `null`).
1751
- 2. Green — add `getConnState` RPC to ConnectionDO; wire `getConnection`.
1752
- 3. Red — WHOIS integration: a remote user returns `311`, not just
1753
- `401`+`318`.
1754
- 4. Green — verify the WHOIS reducer now receives remote state.
1755
- 5. Red — `getChannelConnections(chan)` returns members; currently
1756
- fails (empty `Map`).
1757
- 6. Green — implement the channel-membership enumeration RPC.
1758
- 7. Red — `listChannels()` returns channels; currently fails (`[]`).
1759
- 8. Green — implement the channel registry (design: dedicated DO vs KV
1760
- index).
1761
- 9. Red — `disconnect(remoteConn)` closes the target socket.
1762
- 10. Green — add `disconnect` RPC to ConnectionDO.
1763
- 11. Refactor — share RPC stub-typing across ConnectionDO/ChannelDO/
1764
- RegistryDO.
1765
- - **Out of scope:** CF TCP+TLS adapter (Phase 7 — TICKET-055); this
1766
- ticket is the wss DO path only.
1767
-
1768
- ---
1769
-
1770
- ### TICKET-074 — AWS disconnect: broadcast QUIT to peers
1771
- - **Phase:** 8 (remediates Phase 4)
1772
- - **Points:** 5
1773
- - **Status:** ⬜
1774
- - **Dependencies:** TICKET-040 (AwsRuntime, ✅),
1775
- TICKET-003 (QUIT reducer, ✅)
1776
- - **Description:** PLAN-FIXES #4.
1777
- `packages/aws-adapter/src/aws-runtime.ts:645` — `cleanupConnection`
1778
- explicitly discards `managementApi` (`void managementApi;`) and never
1779
- broadcasts QUIT. Confirmed at the call site:
1780
- `packages/aws-adapter/src/handlers/disconnect.ts:28` hardcodes `null`.
1781
-
1782
- **Impact:** when a connection tears down, peers in shared channels
1783
- never receive the QUIT fanout. Their local rosters go stale until a
1784
- subsequent broadcast prunes the ghost member.
1785
- - **Acceptance Criteria:**
1786
- - `handleDisconnect` (`handlers/disconnect.ts`) threads the
1787
- `managementApi` (or a `PostToConnection`) through to
1788
- `cleanupConnection`.
1789
- - `cleanupConnection` builds a QUIT line for the disconnecting
1790
- connection and, for each channel in `joinedChannels`, fans the QUIT
1791
- out to every other member via `managementApi.PostToConnection`.
1792
- - Gone connections during fanout are caught (`GoneException` path) and
1793
- trigger lazy cleanup — no crash on a partially-torn-down roster.
1794
- - Both the `$disconnect` route and the `$default`/send `GoneException`
1795
- cleanup path emit QUIT fanout.
1796
- - Integration test: two connections sharing a channel; connection A's
1797
- `$disconnect` causes connection B to receive `:A QUIT :reason`.
1798
- - 100% coverage on the new fanout code.
1799
- - **TDD Outline:**
1800
- 1. Red — integration test asserting peer B receives QUIT on A's
1801
- disconnect; currently fails (no fanout).
1802
- 2. Green — thread `managementApi`; implement fanout.
1803
- 3. Red — fanout to a gone peer triggers lazy cleanup, not a crash.
1804
- 4. Green — wrap `PostToConnection` in try/catch; route
1805
- `GoneException` to cleanup.
1806
- 5. Refactor — extract the fanout loop into a shared
1807
- `broadcastQuitToChannels(...)` helper reusable by both
1808
- `$disconnect` and the `GoneException` sweep.
1809
-
1810
- ---
1811
-
1812
- ### TICKET-075 — Implement `OPER` command (verify creds, set oper mode, emit `381`)
1813
- - **Phase:** 8 (remediates Phase 1)
1814
- - **Points:** 5
1815
- - **Status:** ⬜
1816
- - **Dependencies:** TICKET-011 (MODE reducer, ✅),
1817
- TICKET-047 (oper creds config, ✅),
1818
- TICKET-015 (WHOIS reducer exposing `RPL_WHOISOPERATOR`, ✅)
1819
- - **Description:** PLAN-FIXES #6. No `OPER` case in `actor.ts` `route()`
1820
- switch, no reducer, no entry in `packages/irc-core/src/commands/`.
1821
- Referenced everywhere as scaffolding:
1822
-
1823
- - `packages/irc-core/src/commands/mode.ts:15` — comment: *"The oper
1824
- flag (`+o`) is granted via the future OPER command, not via MODE."*
1825
- - `packages/irc-core/src/commands/mode.ts:547, 612-624` — user
1826
- `MODE +o` hard-rejected with `481 ERR_NOPRIVILEGES`, gated on an
1827
- OPER command that does not exist.
1828
- - `operCreds` is defined, validated, and loaded by **all three
1829
- adapters** (`packages/cf-adapter/src/config-loader.ts:65`,
1830
- `packages/aws-adapter/src/config-loader.ts:59`,
1831
- `apps/local-cli/src/config-loader.ts:27`) but no runtime code ever
1832
- reads `serverConfig.operCreds`.
1833
- - Reserved-but-unused numerics: `RPL_YOUREOPER: 381`,
1834
- `RPL_REHASHING: 382`, `ERR_NOOPERHOST: 491`
1835
- (`packages/irc-core/src/numerics.ts:75-76, 138`).
1836
-
1837
- **Impact:** `state.userModes.oper` can never become `true` via any
1838
- command flow, so the `RPL_WHOISOPERATOR` (`313`) branch in
1839
- `packages/irc-core/src/commands/whois.ts:132-136` and the WHO `*`
1840
- oper flag (`packages/irc-core/src/commands/who.ts:69`) are
1841
- unreachable.
1842
- - **Acceptance Criteria:**
1843
- - A new `operReducer` in `packages/irc-core/src/commands/oper.ts`:
1844
- parses `OPER <name> <password>`, verifies against
1845
- `serverConfig.operCreds`, on success sets `state.userModes.oper =
1846
- true` and emits `381 RPL_YOUREOPER`.
1847
- - Wrong password / unknown name → `464 ERR_PASSWELCOME` (or `491
1848
- ERR_NOOPERHOST` if no creds configured).
1849
- - Already-oper → graceful no-op or `462`-style reply.
1850
- - Route case `OPER` added to `actor.ts` switch.
1851
- - `RPL_WHOISOPERATOR (313)` branch in `whois.ts` now reachable: a
1852
- WHOIS of an oper returns `313`.
1853
- - WHO `*` oper flag (`who.ts:69`) now reachable.
1854
- - `mode.ts:612-624` user `MODE +o` rejection stays (oper is set via
1855
- OPER, not MODE); the comment at `mode.ts:15` is updated to past
1856
- tense now that OPER exists.
1857
- - 100% coverage on `operReducer`; existing whois/who oper-branch tests
1858
- updated to use a real oper.
1859
- - **TDD Outline:**
1860
- 1. Red — reducer test: `OPER alice secret` against a config with
1861
- `operCreds: [{name:'alice', password:'secret'}]` sets `oper:true`
1862
- and emits `381`; currently fails (no reducer).
1863
- 2. Green — implement `operReducer`.
1864
- 3. Red — wrong password → `464`; no creds configured → `491`.
1865
- 4. Green — cover error paths.
1866
- 5. Red — actor test: routed `OPER` does not return `421`.
1867
- 6. Green — add the route case.
1868
- 7. Red — WHOIS of an oper returns `313`.
1869
- 8. Green — verify the existing whois branch now lights up.
1870
- 9. Refactor — extract credential-match into a pure helper.
1871
- - **Out of scope:** oper-gated verbs `KILL`/`REHASH`/`CONNECT`/`SQUIT`/
1872
- `TRACE` (TICKET-078).
1873
-
1874
- ---
1875
-
1876
- ### TICKET-076 — Implement `TAGMSG` command (IRCv3 `message-tags` recorder)
1877
- - **Phase:** 8 (remediates Phase 1)
1878
- - **Points:** 3
1879
- - **Status:** ⬜
1880
- - **Dependencies:** TICKET-018 (`message-tags`, ✅),
1881
- TICKET-063 (chathistory, ✅), TICKET-070 (`MessageStore` wiring)
1882
- - **Description:** PLAN-FIXES #7. `StoredCommand = 'PRIVMSG' | 'NOTICE' |
1883
- 'TAGMSG'` (`packages/irc-core/src/ports.ts:200`) and
1884
- `packages/irc-core/src/commands/chathistory.ts:237-238` contain real
1885
- replay code for a stored TAGMSG. But nothing ever records one: the
1886
- only call to `ctx.messages.record(...)` is
1887
- `packages/irc-core/src/commands/privmsg.ts:188`, which records
1888
- PRIVMSG/NOTICE only.
1889
-
1890
- **Impact:** the `message-tags` cap is advertised
1891
- (`packages/irc-core/src/caps/capabilities.ts:33`) with no command that
1892
- consumes it. The TAGMSG replay branch in `chathistory.ts` is dead
1893
- code.
1894
- - **Acceptance Criteria:**
1895
- - A `TAGMSG` reducer (new file or extension to `privmsg.ts`) handles
1896
- `TAGMSG <target> +tag1=value1;tag2`.
1897
- - Channel TAGMSG broadcasts to members (excluding sender unless
1898
- `echo-message` is negotiated) carrying the tag set.
1899
- - Private TAGMSG to a user routes via `LookupNick`.
1900
- - Channel TAGMSG records via `ctx.messages.record({ command:
1901
- 'TAGMSG', ... })` when a `MessageStore` is bound (the data source
1902
- for chathistory replay).
1903
- - NOTICE-style rule: TAGMSG MUST NOT trigger numeric error replies
1904
- (it is a stateless tag broadcast per the IRCv3 spec).
1905
- - Without the `message-tags` cap: the command is `421
1906
- ERR_UNKNOWNCOMMAND` (per spec).
1907
- - Route case `TAGMSG` added to `actor.ts`.
1908
- - chathistory replay now returns recorded TAGMSGs in BATCH output.
1909
- - 100% coverage on the new reducer and route case.
1910
- - **TDD Outline:**
1911
- 1. Red — reducer test: `TAGMSG #foo +foo=bar` to a channel broadcasts
1912
- the line; currently fails (no reducer).
1913
- 2. Green — implement the `TAGMSG` reducer.
1914
- 3. Red — recording: with a bound `MessageStore`,
1915
- `ctx.messages.record` receives a TAGMSG entry.
1916
- 4. Green — wire the record path.
1917
- 5. Red — actor test: `TAGMSG` routes (no `421`).
1918
- 6. Green — add the route case.
1919
- 7. Red — chathistory LATEST returns recorded TAGMSGs.
1920
- 8. Refactor — share the broadcast/record loop with `privmsg.ts`.
1921
-
1922
- ---
1923
-
1924
- ### TICKET-077 — Implement `WHOWAS` (or drop the numerics)
1925
- - **Phase:** 8 (remediates Phase 1)
1926
- - **Points:** 5
1927
- - **Status:** ⬜
1928
- - **Dependencies:** TICKET-015 (WHOIS reducer, ✅),
1929
- TICKET-003 (QUIT reducer, ✅)
1930
- - **Description:** PLAN-FIXES #8. `RPL_ENDOFWHOWAS: 369`,
1931
- `ERR_WASNOSUCHNICK: 406` are defined
1932
- (`packages/irc-core/src/numerics.ts:65, 97`); no reducer, no route
1933
- case.
1934
- - **Acceptance Criteria:**
1935
- - Decision documented in the ticket PR: either implement WHOWAS or
1936
- remove the numerics.
1937
- - If implementing:
1938
- - A nick-history store (port + in-memory reference impl): records
1939
- `(nick, connId, username, hostname, signoffTime)` on
1940
- QUIT/NICK-change; purged past a configurable TTL.
1941
- - `WHOWAS <nick>{,<nick>} [<count>]` queries the store; emits `314
1942
- RPL_WHOWASUSER` per match, `369 RPL_ENDOFWHOWAS` terminator, `406
1943
- ERR_WASNOSUCHNICK` when no match.
1944
- - Route case `WHOWAS` added to `actor.ts`.
1945
- - If dropping: remove `RPL_ENDOFWHOWAS`/`ERR_WASNOSUCHNICK` from
1946
- `numerics.ts`; update any tests referencing them; note the decision
1947
- in `docs/`.
1948
- - 100% coverage on whichever path ships.
1949
- - **TDD Outline:**
1950
- 1. Decision spike — pick implement-vs-drop; document in the ticket PR.
1951
- 2. Red (if implementing) — reducer test: `WHOWAS alice` after alice
1952
- QUIT returns `314`; currently fails (no reducer).
1953
- 3. Green — implement `whowasReducer` + the nick-history store.
1954
- 4. Red — `WHOWAS unknownnick` → `406`.
1955
- 5. Green — cover the no-match path.
1956
- 6. Red — actor test: `WHOWAS` routes.
1957
- 7. Green — add the route case.
1958
- 8. Refactor — share nick-history plumbing with the QUIT/NICK-change
1959
- reducers that populate it.
1960
-
1961
- ---
1962
-
1963
- ### TICKET-078 — Implement (or drop) remaining standard IRC verbs
1964
- - **Phase:** 8 (remediates Phase 1)
1965
- - **Points:** 5
1966
- - **Status:** ⬜
1967
- - **Dependencies:** TICKET-030 (actor, ✅),
1968
- TICKET-047 (config, ✅),
1969
- TICKET-075 (oper — gates oper-only verbs)
1970
- - **Description:** PLAN-FIXES #9. Commands currently returning `421
1971
- ERR_UNKNOWNCOMMAND` via `actor.ts:318-319` (graceful, not broken):
1972
-
1973
- `VERSION`, `ADMIN`, `INFO`, `TIME`, `STATS`, `LUSERS`, `LINKS`,
1974
- `USERHOST`, `ISON`, `WALLOPS`, `SETNAME`, `KILL`, `REHASH`,
1975
- `CONNECT`, `TRACE`, `SQUIT`, `SERVICE`, `SUMMON`, `USERS`.
1976
-
1977
- Several have dedicated numerics defined but unused: `RPL_USERHOST:
1978
- 302`, `RPL_ISON: 303`, `RPL_INFO: 371`, `RPL_LINKS: 364`,
1979
- `RPL_TIME: 391`, `RPL_ADMINME: 256`.
1980
- - **Acceptance Criteria:**
1981
- - Triage documented in the ticket PR: classify each verb as
1982
- implement / drop / defer. Recommended minimum viable set to
1983
- implement: `VERSION` (uses `SERVER_VERSION` + `SERVER_NAME`),
1984
- `TIME` (uses `Clock`), `USERHOST` (uses `ConnectionState`),
1985
- `ISON` (uses `ConnectionState`/registry), `LUSERS` (uses runtime
1986
- stats).
1987
- - For each implemented verb: a reducer in
1988
- `packages/irc-core/src/commands/`, a route case in `actor.ts`, and
1989
- spec-correct numeric replies.
1990
- - For each dropped verb: either remove the unused numerics (if
1991
- exclusive to that verb) or leave a comment explaining why the
1992
- numeric is retained.
1993
- - `KILL`, `REHASH`, `CONNECT`, `SQUIT`, `TRACE` — oper-gated (depend
1994
- on TICKET-075); implement only if oper has landed, otherwise defer
1995
- with a documented note.
1996
- - 100% coverage on each implemented reducer.
1997
- - **TDD Outline:**
1998
- 1. Red — one test per implemented verb (e.g. `VERSION` returns `351`
1999
- with the configured version); currently fails (`421`).
2000
- 2. Green — implement each reducer; add the route case.
2001
- 3. Red — oper-gated verbs return `481` for non-opers
2002
- (post-TICKET-075).
2003
- 4. Refactor — share a "config-derived reply" helper across
2004
- `VERSION`/`ADMIN`/`INFO`.
2005
-
2006
- ---
2007
-
2008
- ### TICKET-079 — SASL `EXTERNAL`: implement mTLS-backed auth OR stop advertising
2009
- - **Phase:** 8 (remediates Phase 5; supersedes the EXTERNAL stub in
2010
- TICKET-026)
2011
- - **Points:** 2
2012
- - **Status:** ⬜
2013
- - **Dependencies:** TICKET-026 (SASL reducer, ✅)
2014
- - **Description:** PLAN-FIXES #10.
2015
- `packages/irc-core/src/commands/sasl.ts:95-97` recognizes
2016
- `AUTHENTICATE EXTERNAL` but unconditionally returns `904
2017
- ERR_SASLFAIL`. The `sasl` cap advertises `PLAIN,EXTERNAL`
2018
- (`packages/irc-core/src/caps/capabilities.ts:36`,
2019
- `packages/irc-core/src/commands/sasl.ts:42`
2020
- `SUPPORTED_MECHS = 'PLAIN,EXTERNAL'`).
2021
-
2022
- **Impact:** EXTERNAL is advertised but non-functional. (PLAIN is fully
2023
- implemented but see TICKET-071.)
2024
-
2025
- Full mTLS implementation is Phase 7 (TICKET-054). This ticket is the
2026
- interim decision: either implement EXTERNAL against a pluggable
2027
- cert-subject provider, OR stop advertising it until 054 lands.
2028
- - **Acceptance Criteria:**
2029
- - Decision documented in the ticket PR: implement OR stop-advertising.
2030
- - If stop-advertising (recommended interim):
2031
- - `SUPPORTED_MECHS` becomes `'PLAIN'`.
2032
- - The `sasl` cap value is updated wherever advertised.
2033
- - `AUTHENTICATE EXTERNAL` now returns `908 ERR_SASLMECHS`
2034
- (supported-mechs list) instead of `904`.
2035
- - A code comment cross-references TICKET-054 so the mech is
2036
- re-enabled when mTLS lands.
2037
- - If implementing (only if a pluggable `MtlsIdentityProvider` is
2038
- feasible without Phase 7 infra):
2039
- - `AccountStore.verify('EXTERNAL', certSubject)` resolves the cert
2040
- subject to an account.
2041
- - `AUTHENTICATE EXTERNAL` with a valid subject succeeds (`903`);
2042
- invalid → `904`.
2043
- - 100% coverage on the chosen path.
2044
- - **TDD Outline:**
2045
- 1. Red (stop-advertising) — test asserting `CAP LS sasl` value is
2046
- `PLAIN`; currently fails (advertises `PLAIN,EXTERNAL`).
2047
- 2. Green — change `SUPPORTED_MECHS`.
2048
- 3. Red — `AUTHENTICATE EXTERNAL` returns `908` (not `904`).
2049
- 4. Green — update the mech-not-supported branch.
2050
- 5. Refactor — drive `SUPPORTED_MECHS` from config so operators can
2051
- enable EXTERNAL without code changes once 054 lands.
2052
-
2053
- ---
2054
-
2055
- ### TICKET-080 — Welcome MOTD: drive from `MotdProvider` instead of hardcoded placeholder
2056
- - **Phase:** 8 (remediates Phase 5)
2057
- - **Points:** 2
2058
- - **Status:** ⬜
2059
- - **Dependencies:** TICKET-004 (registration reducer, ✅),
2060
- TICKET-010 (MOTD reducer, ✅)
2061
- - **Description:** PLAN-FIXES #11.
2062
- `packages/irc-core/src/commands/registration.ts:84-87` always emits:
2063
-
2064
- ```
2065
- 375 <nick> :- <server> Message of the day -
2066
- 372 <nick> :- MOTD placeholder
2067
- 376 <nick> :End of MOTD command
2068
- ```
2069
-
2070
- regardless of the configured `MotdProvider`. The comment claims
2071
- "filled via `MotdProvider` elsewhere" but it is not. The standalone
2072
- `MOTD` command correctly uses `ctx.motd.lines()`
2073
- (`packages/irc-core/src/commands/motd.ts:59`).
2074
-
2075
- **Impact:** every client sees `372 <nick> :- MOTD placeholder` on
2076
- connect regardless of configuration.
2077
- - **Acceptance Criteria:**
2078
- - `buildWelcomeLines` calls `ctx.motd.lines()` and emits the real
2079
- `375`/`372`×N/`376` sequence (mirroring `motd.ts:59`).
2080
- - Empty MOTD → `422 ERR_NOMOTD` instead of the placeholder (matches
2081
- standalone MOTD behaviour).
2082
- - Long lines split per the byte limit (same helper as `motd.ts`).
2083
- - The standalone `MOTD` command behaviour is unchanged.
2084
- - 100% coverage on the new path.
2085
- - **TDD Outline:**
2086
- 1. Red — registration test: with a `MotdProvider` returning
2087
- `['line A','line B']`, the welcome sequence contains two `372`
2088
- lines; currently fails (always `MOTD placeholder`).
2089
- 2. Green — call `ctx.motd.lines()` in `buildWelcomeLines`.
2090
- 3. Red — empty MOTD → `422`.
2091
- 4. Green — handle the empty case.
2092
- 5. Refactor — extract the MOTD-lines→numerics helper shared between
2093
- `registration.ts` and `motd.ts`.
2094
-
2095
- ---
2096
-
2097
- ### TICKET-081 — Registration placeholders: drive `SERVER_VERSION` and `CREATED_TEXT` from `ServerConfig`
2098
- - **Phase:** 8 (remediates Phase 5)
2099
- - **Points:** 3
2100
- - **Status:** ⬜
2101
- - **Dependencies:** TICKET-004 (registration, ✅),
2102
- TICKET-047 (config, ✅)
2103
- - **Description:** PLAN-FIXES #12.
2104
- `packages/irc-core/src/commands/registration.ts:29, 32`:
2105
-
2106
- - `SERVER_VERSION = '0.1.0'` — *"Placeholder until a config-driven
2107
- version lands."*
2108
- - `CREATED_TEXT = 'Phase 1'` — *"Placeholder."*
2109
-
2110
- Surfaced verbatim in `002`/`003`/`004` replies.
2111
- - **Acceptance Criteria:**
2112
- - `ServerConfig` gains optional `serverVersion?: string` and
2113
- `createdAt?: string | number` fields.
2114
- - When `serverVersion` is omitted, the build process injects the
2115
- `package.json` version at build time — or the field is required at
2116
- adapter config-load time (recommended fail-fast to match
2117
- TICKET-082).
2118
- - When `createdAt` is omitted, a sensible default is used (build
2119
- timestamp or deploy-time).
2120
- - The `002`/`003`/`004` replies reflect the configured/injected
2121
- values, not the literals `'0.1.0'` / `'Phase 1'`.
2122
- - The placeholder constants are removed from `registration.ts`.
2123
- - Zod schema updated; both adapters' config loaders pass the new
2124
- fields.
2125
- - 100% coverage on the new plumbing.
2126
- - **TDD Outline:**
2127
- 1. Red — registration test: with `serverVersion: '9.9.9'`, the `002`
2128
- line contains `9.9.9`; currently fails (always `0.1.0`).
2129
- 2. Green — thread the config field into `buildWelcomeLines`.
2130
- 3. Red — `createdAt` flows into `003`.
2131
- 4. Green — implement.
2132
- 5. Red — schema rejects malformed `serverVersion`.
2133
- 6. Refactor — share config-driven-version resolution across adapters.
2134
-
2135
- ---
2136
-
2137
- ### TICKET-082 — Require `serverName` at adapter config-load (drop `irc.example.com` placeholder)
2138
- - **Phase:** 8 (remediates Phase 5)
2139
- - **Points:** 3
2140
- - **Status:** ⬜
2141
- - **Dependencies:** TICKET-047 (config, ✅)
2142
- - **Description:** PLAN-FIXES #13.
2143
- `packages/irc-core/src/config.ts:244-247` — `serverName:
2144
- 'irc.example.com'` as the baseline. Documented as a boot/test
2145
- baseline that adapters overlay, and `docs/AWS-Deployment.md:414` warns
2146
- *"(placeholder -- override!)"*.
2147
-
2148
- **Impact:** if a deployment forgets to override, the server advertises
2149
- `irc.example.com`.
2150
- - **Acceptance Criteria:**
2151
- - `DEFAULT_SERVER_CONFIG.serverName` is removed (or set to a sentinel
2152
- that triggers fail-fast).
2153
- - Each adapter's config loader (CF / AWS / local-cli) requires
2154
- `serverName` at boot; missing → fail with a readable error pointing
2155
- at the config knob.
2156
- - Tests can still construct a `ServerConfig` without `serverName` by
2157
- passing an explicit test-only value (test helpers updated).
2158
- - `docs/AWS-Deployment.md` and `docs/deployment-cf.md` updated:
2159
- `serverName` is required, no longer "placeholder -- override!".
2160
- - 100% coverage on the fail-fast path per adapter.
2161
- - **TDD Outline:**
2162
- 1. Red — adapter loader test: missing `serverName` throws with a
2163
- readable message; currently fails (defaults to `irc.example.com`).
2164
- 2. Green — remove the default; add the validation.
2165
- 3. Red — test-only configs that omit `serverName` still work via a
2166
- test helper.
2167
- 4. Green — provide the helper.
2168
- 5. Refactor — share the validation across the three loaders.
2169
-
2170
- ---
2171
-
2172
- ### TICKET-083 — AWS `handleConnect`: implement max-clients admission gating
2173
- - **Phase:** 8 (remediates Phase 4)
2174
- - **Points:** 5
2175
- - **Status:** ⬜
2176
- - **Dependencies:** TICKET-040 (AwsRuntime, ✅),
2177
- TICKET-046 (security, ✅)
2178
- - **Description:** PLAN-FIXES #14.
2179
- `packages/aws-adapter/src/handlers/connect.ts:36` —
2180
- `void params.serverConfig;` (*"reserved for max-clients gating"*).
2181
-
2182
- **Impact:** connection-count admission gating at connect time is
2183
- unimplemented.
2184
- - **Acceptance Criteria:**
2185
- - `handleConnect` reads `serverConfig.maxClients` (and optionally
2186
- per-IP limits) and rejects new connections over the cap with an
2187
- APIGW `403 Forbidden` (or `429`) before the connection is recorded
2188
- in DynamoDB.
2189
- - The cap is enforced atomically against the `Connections` table
2190
- (e.g. a count query or a lever on table capacity).
2191
- - Per-IP limit (optional) keys on the source IP from the APIGW event.
2192
- - Documented default cap (e.g. 10000) and override knob in config.
2193
- - Integration test: `cap=2`, three connects → third rejected; the two
2194
- accepted connections function normally.
2195
- - 100% coverage on the admission path.
2196
- - **TDD Outline:**
2197
- 1. Red — integration test: cap exceeded → third connect rejected;
2198
- currently fails (all accepted).
2199
- 2. Green — read `maxClients`, count `Connections`, reject over cap.
2200
- 3. Red — per-IP limit enforced.
2201
- 4. Green — implement the per-IP counter.
2202
- 5. Refactor — extract the admission decision into a pure helper.
2203
-
2204
- ---
2205
-
2206
- ### TICKET-084 — AWS Lambda transport `disconnect` no-op: document the platform limit
2207
- - **Phase:** 8 (remediates Phase 4)
2208
- - **Points:** 1
2209
- - **Status:** ⬜
2210
- - **Dependencies:** TICKET-040 (AwsRuntime, ✅)
2211
- - **Description:** PLAN-FIXES #15.
2212
- `packages/aws-adapter/src/handlers/default.ts:86-89` —
2213
- `AwsRuntimeHandlers.disconnect` does nothing. Documented as a
2214
- platform limit (a Lambda cannot close its own APIGW socket; the
2215
- canonical close is the `$disconnect` route).
2216
-
2217
- **Impact:** effect-driven `Disconnect` on the bound connection never
2218
- reaches the transport from within a `$default` invocation. No code
2219
- fix is possible without an APIGW feature; this ticket is
2220
- documentation only.
2221
- - **Acceptance Criteria:**
2222
- - `docs/AWS-Deployment.md` gains a "Limitations" section explaining
2223
- that a Lambda `$default` invocation cannot close its own APIGW
2224
- socket; the canonical teardown is the `$disconnect` route (or a
2225
- callback to a sidecar sweeper).
2226
- - The stale comment at `handlers/default.ts:86-89` is updated to
2227
- cross-reference the doc section.
2228
- - Optional: explore an APIGW callback / EventBridge path to
2229
- force-close from within `$default`; documented as a future
2230
- enhancement or ADR if promising.
2231
- - **TDD Outline:**
2232
- - N/A (documentation). Reviewed by reading the doc section and
2233
- confirming it explains the limit clearly to an operator.
2234
-
2235
- ---
2236
-
2237
- ### TICKET-085 — CF `webSocketMessage` channel-registration: wire ChannelDO registration OR remove the dead loop
2238
- - **Phase:** 8 (remediates Phase 3)
2239
- - **Points:** 2
2240
- - **Status:** ⬜
2241
- - **Dependencies:** TICKET-035 (ChannelDO, ✅)
2242
- - **Description:** PLAN-FIXES #16.
2243
- `packages/cf-adapter/src/connection-do.ts:150-156` — the loop body is
2244
- `void chan;`.
2245
-
2246
- **Impact:** no real channel-registration work happens on join beyond
2247
- the `ApplyChannelDelta` already emitted by the reducer.
2248
- - **Acceptance Criteria:**
2249
- - Decision documented: wire real channel-DO registration here, OR
2250
- remove the dead loop.
2251
- - If wiring: the loop registers the ConnectionDO with each ChannelDO
2252
- it joins (so the ChannelDO can fan out broadcasts to it). Verify
2253
- this isn't already handled by `applyChannelDelta` (if it is, remove
2254
- the loop and add a comment explaining the redundancy).
2255
- - If removing: the dead loop and its `void chan;` body are deleted; a
2256
- comment explains that channel membership is driven solely by
2257
- `ApplyChannelDelta`.
2258
- - 100% coverage on whichever path ships.
2259
- - **TDD Outline:**
2260
- 1. Spike — confirm whether `ApplyChannelDelta` already covers
2261
- channel-DO registration (read the ChannelDO reducer).
2262
- 2. Red (if removing) — test asserting the loop body is gone
2263
- (compile-time / lint check).
2264
- 3. Green — delete the dead code.
2265
- 4. Red (if wiring) — integration test: joining a channel makes the
2266
- ConnectionDO discoverable to ChannelDO broadcast.
2267
- 5. Green — wire the registration.
2268
-
2269
- ---
2270
-
2271
- ### TICKET-086 — Remove (or wire) dead lookup Effects: `LookupNick`, `GetConnectionInfo`, `GetChannelSnapshot`
2272
- - **Phase:** 8 (remediates Phase 6)
2273
- - **Points:** 2
2274
- - **Status:** ⬜
2275
- - **Dependencies:** TICKET-001 (Effect taxonomy, ✅)
2276
- - **Description:** PLAN-FIXES #17. `LookupNick`, `GetConnectionInfo`,
2277
- `GetChannelSnapshot` are defined in
2278
- `packages/irc-core/src/effects.ts:74-87` and `:170-178` (and included
2279
- in the `Effect` union at `:112-114`), but their dispatch results are
2280
- discarded via `noop`
2281
- (`packages/irc-server/src/dispatch.ts:36-38`) and no reducer ever
2282
- emits them. The runtime methods they wrap
2283
- (`lookupNick`/`getConnectionInfo`/`getChannelSnapshot`) are used
2284
- elsewhere, so only the Effect path is dead.
2285
- - **Acceptance Criteria:**
2286
- - Decision documented: wire these effects to real consumers, OR
2287
- remove the variants and their dispatch arms.
2288
- - If removing: delete the three Effect variants from `effects.ts`,
2289
- remove their cases from `dispatch.ts`, and remove from the `Effect`
2290
- union. Confirm no reducer/test references them.
2291
- - If wiring: identify a reducer that should emit them (e.g. PRIVMSG
2292
- to a user emitting `LookupNick` instead of calling
2293
- `runtime.lookupNick` directly); implement the dispatch resolution;
2294
- add a test asserting the effect round-trips through dispatch.
2295
- - 100% coverage on whichever path ships; existing dispatch tests
2296
- updated.
2297
- - **TDD Outline:**
2298
- 1. Spike — grep for any emit site of the three effects (expected:
2299
- none).
2300
- 2. Red (if removing) — compile error after deletion is the "test";
2301
- existing dispatch tests adjusted.
2302
- 3. Green — delete the dead variants.
2303
- 4. Red (if wiring) — a reducer emits `LookupNick` and dispatch
2304
- resolves it.
2305
- 5. Green — wire the consumer.
2306
- 6. Refactor — consolidate the dispatch table.
2307
-
2308
- ---
2309
-
2310
- ### TICKET-087 — CF outbound batching: implement OR drop the reserved optimization
2311
- - **Phase:** 8 (remediates Phase 6)
2312
- - **Points:** 2
2313
- - **Status:** ⬜
2314
- - **Dependencies:** TICKET-033 (ConnectionDO, ✅)
2315
- - **Description:** PLAN-FIXES #18.
2316
- `packages/cf-adapter/src/connection-do.ts:100-101` — a field for
2317
- batching outbound line drains is commented out, explicitly deferred.
2318
- - **Acceptance Criteria:**
2319
- - Decision documented: implement the batching optimization, OR drop
2320
- the reserved comment.
2321
- - If implementing: a buffered outbound drain that coalesces multiple
2322
- lines into a single `WebSocket.send()` per microtask (or per alarm
2323
- tick); benchmark showing reduced WS round-trips under burst load;
2324
- correctness test that line ordering is preserved.
2325
- - If dropping: the commented-out field is removed; a brief note in a
2326
- perf-relevant ADR or doc explains why batching was deferred (e.g.
2327
- *"out of scope for v1; revisit if WS round-trips become a
2328
- bottleneck — see load-test report TICKET-049"*).
2329
- - 100% coverage on whichever path ships.
2330
- - **TDD Outline:**
2331
- 1. Spike — measure current WS round-trips per PRIVMSG burst (informs
2332
- implement-vs-drop).
2333
- 2. Red (if implementing) — test: N broadcasts in one tick result in 1
2334
- `WebSocket.send` call (currently N).
2335
- 3. Green — implement the buffered drain.
2336
- 4. Red (if dropping) — the field is gone (compile-time check).
2337
- 5. Green — delete.
2338
- 6. Refactor — share the buffering logic with the AWS adapter if both
2339
- end up batching.
2340
-
2341
- ---
2342
-
2343
- ### TICKET-088 — Update stale doc-comments advertising DOs as stubbed
2344
- - **Phase:** 8 (remediates Phase 6)
2345
- - **Points:** 2
2346
- - **Status:** ⬜
2347
- - **Dependencies:** TICKET-035 (ChannelDO, ✅),
2348
- TICKET-034 (RegistryDO, ✅)
2349
- - **Description:** PLAN-FIXES #19.
2350
- `packages/cf-adapter/src/env.ts:23-36` — still describes `REGISTRY_DO`
2351
- as *"Stubbed now (033); real impl in 034"* and `CHANNEL_DO` as
2352
- *"Stubbed now; real in 035"*, even though `registry-do.ts` and
2353
- `channel-do.ts` now contain real implementations.
2354
-
2355
- `packages/cf-adapter/src/connection-do.ts:148-153, 291-293, 337-338,
2356
- 504-506, 549` contain similar stale "stub" language; `:546-549`
2357
- exports `applyChannelDelta` purely to keep an import used by tests.
2358
- - **Acceptance Criteria:**
2359
- - All stale "stub" / "real in 0XX" comments in `env.ts` and
2360
- `connection-do.ts` updated to reflect the implemented state.
2361
- - The test-only re-export of `applyChannelDelta`
2362
- (`connection-do.ts:546-549`) is either moved behind a test boundary
2363
- (e.g. a `__test__` export or a separate `internal.ts` consumed only
2364
- by tests) or removed if tests can import directly.
2365
- - No functional change; full test suite stays green.
2366
- - **TDD Outline:**
2367
- 1. Red — lint/test asserting no occurrences of "Stubbed now" or
2368
- "real in 0" in `packages/cf-adapter/src/*.ts` (custom grep test or
2369
- ESLint rule).
2370
- 2. Green — rewrite the comments.
2371
- 3. Red — test asserting `applyChannelDelta` is not part of the public
2372
- `connection-do.ts` surface.
2373
- 4. Green — move the export behind a test boundary.
2374
- 5. Refactor — centralize DO-status documentation in a single ADR or
2375
- doc section.
2376
-
2377
- ---
2378
-
2379
- ### TICKET-089 — Implement rfc1459 case-mapping (replace ad-hoc `toLowerCase`)
2380
- - **Phase:** 8 (remediates Phase 6)
2381
- - **Points:** 5
2382
- - **Status:** ⬜
2383
- - **Dependencies:** TICKET-011 (MODE reducer, ✅)
2384
- - **Description:** PLAN-FIXES #20. `packages/irc-core/src/isupport.ts:43`
2385
- advertises `CASEMAPPING=rfc1459`, but
2386
- `packages/irc-core/src/commands/kick.ts:57-60` (and the duplicated
2387
- `lowerNick`/`lower` helpers in `mode.ts`, `invite.ts`, `join.ts`,
2388
- `packages/in-memory-runtime/src/in-memory-runtime.ts`) use bare ASCII
2389
- `toLowerCase()`. The advertised case-mapping and the actual folding
2390
- diverge for the `[]\^` special chars.
2391
- - **Acceptance Criteria:**
2392
- - A single shared `caseFold(mapping, s)` helper in `irc-core`
2393
- (supports at least `rfc1459`; `ascii` as a subset).
2394
- - All nick/channel-name lowering call sites (`kick.ts`, `mode.ts`,
2395
- `invite.ts`, `join.ts`, `in-memory-runtime.ts`, and any other
2396
- `lower*`/`toLowerCase` sites used for IRC-name comparison) use the
2397
- shared helper.
2398
- - `[]\^` and `{}|~` fold correctly per rfc1459 (`[` ↔ `{`, etc.).
2399
- - The `CASEMAPPING=` token in isupport now matches actual behaviour.
2400
- - 100% coverage on the helper; property-test round-trip
2401
- (`fold(fold(s)) === fold(s)`).
2402
- - Existing reducer tests pass unchanged (their inputs happen to be
2403
- ASCII-safe; new tests cover the special chars).
2404
- - **TDD Outline:**
2405
- 1. Red — unit test: `caseFold('rfc1459', '[Foo]')` equals `'{foo}'`;
2406
- currently fails (no helper, or `toLowerCase` yields `[foo]`).
2407
- 2. Green — implement the helper.
2408
- 3. Red — channel comparison test: `JOIN #foo` then `JOIN #FOO` are
2409
- the same channel under rfc1459 (currently they may diverge for
2410
- `[`-containing names).
2411
- 4. Green — replace ad-hoc `toLowerCase` call sites with the helper.
2412
- 5. Red — property test: `fold(fold(s)) === fold(s)` for generated
2413
- inputs.
2414
- 6. Refactor — delete the duplicated `lowerNick`/`lower` helpers.
2415
-
2416
- ---
2417
-
2418
- ### TICKET-090 — Remove unused `_disabledRecordId()` helper
2419
- - **Phase:** 8 (remediates Phase 6)
2420
- - **Points:** 1
2421
- - **Status:** ⬜
2422
- - **Dependencies:** TICKET-029 (in-memory-runtime, ✅)
2423
- - **Description:** PLAN-FIXES #21.
2424
- `packages/in-memory-runtime/src/in-memory-runtime.ts:61-69` — mints
2425
- admission record ids whose own doc comment states *"The id is never
2426
- consulted (no `AdmissionStats` exists)."* Called at `:116` when
2427
- admission is disabled; the returned `recordId` flows nowhere.
2428
- - **Acceptance Criteria:**
2429
- - `_disabledRecordId()` helper deleted; its call site at `:116`
2430
- removed.
2431
- - If an `AdmissionStats` consumer is desired, file a separate
2432
- follow-up; otherwise the helper and the admission-record concept are
2433
- gone from the codebase.
2434
- - Full test suite stays green (the helper was never asserted).
2435
- - 100% coverage on the cleaned-up admission path.
2436
- - **TDD Outline:**
2437
- 1. Red — grep/compile test asserting no references to
2438
- `_disabledRecordId`.
2439
- 2. Green — delete the helper and its call site.
2440
- 3. Refactor — simplify the disabled-admission branch.
2441
-
2442
- ---
2443
-
2444
- ### TICKET-091 — Update stale CLI doc comment ("fuller config-loader lands in a later ticket")
2445
- - **Phase:** 8 (remediates Phase 6)
2446
- - **Points:** 1
2447
- - **Status:** ⬜
2448
- - **Dependencies:** TICKET-031 (local-cli, ✅)
2449
- - **Description:** PLAN-FIXES #22. `apps/local-cli/src/main.ts:17` —
2450
- *"a fuller config-loader lands in a later ticket."*
2451
- `apps/local-cli/src/config-loader.ts` already exists and fully
2452
- validates via `ServerConfigSchema`.
2453
- - **Acceptance Criteria:**
2454
- - The stale comment at `main.ts:17` is removed or rewritten to
2455
- reflect that config loading is fully implemented via
2456
- `apps/local-cli/src/config-loader.ts`.
2457
- - No functional change.
2458
- - **TDD Outline:**
2459
- - N/A (comment-only). Verified by reading the updated comment and
2460
- confirming `config-loader.ts` is exercised by the CLI's tests.
2461
-
2462
- ---
2463
-
2464
- ### TICKET-092 — Update stale release-process doc (90% coverage gate IS wired in CI)
2465
- - **Phase:** 8 (remediates Phase 6)
2466
- - **Points:** 1
2467
- - **Status:** ⬜
2468
- - **Dependencies:** TICKET-048 (CI coverage gate, ✅)
2469
- - **Description:** PLAN-FIXES #23. `docs/Release-Process.md:114` —
2470
- *"the 90% coverage gate (TICKET-048) is not yet wired in CI."*
2471
- `.github/workflows/ci.yml:57-60` shows the gate **is** wired and
2472
- enforced per package (100% on `irc-core`/`irc-server`/
2473
- `in-memory-runtime`, >=90% elsewhere).
2474
- - **Acceptance Criteria:**
2475
- - `docs/Release-Process.md` updated: the coverage gate is enforced in
2476
- CI per package; the exact thresholds (100% / ≥90%) are stated.
2477
- - Cross-reference to `.github/workflows/ci.yml` and to TICKET-048.
2478
- - No functional change.
2479
- - **TDD Outline:**
2480
- - N/A (documentation). Verified by reading the updated doc and
2481
- confirming the CI workflow matches the stated thresholds.
2482
-
2483
- ---
2484
-
2485
- *Next ticket number: **TICKET-093**.*