serverless-ircd 0.1.0 → 0.2.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.
- package/.github/workflows/ci.yml +96 -2
- package/.github/workflows/deploy-aws.yml +129 -0
- package/.github/workflows/deploy-cf.yml +0 -2
- package/.gitmodules +3 -0
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +352 -0
- package/MOTD.txt +3 -0
- package/PLAN-FIXES.md +358 -0
- package/PLAN.md +420 -0
- package/README.md +115 -81
- package/apps/aws-stack/README.md +73 -0
- package/apps/aws-stack/bin/aws.ts +49 -0
- package/apps/aws-stack/cdk.json +10 -0
- package/apps/aws-stack/package.json +41 -0
- package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
- package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
- package/apps/aws-stack/scripts/smoke.mjs +142 -0
- package/apps/aws-stack/src/aws-stack.ts +263 -0
- package/apps/aws-stack/src/tables.ts +10 -0
- package/apps/aws-stack/tests/localstack.test.ts +46 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
- package/apps/aws-stack/tests/stack.test.ts +464 -0
- package/apps/aws-stack/tsconfig.build.json +11 -0
- package/apps/aws-stack/tsconfig.test.json +8 -0
- package/apps/aws-stack/vitest.config.ts +20 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.toml +25 -0
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +56 -0
- package/apps/local-cli/src/server.ts +241 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +74 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- package/dashboards/cloudwatch-irc.json +139 -0
- package/docs/AWS-Adapter-Architecture.md +494 -0
- package/docs/AWS-Deployment.md +1107 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +1 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +440 -0
- package/package.json +8 -2
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/aws-runtime.ts +783 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +69 -0
- package/packages/aws-adapter/src/config-loader.ts +96 -0
- package/packages/aws-adapter/src/dynamo.ts +61 -0
- package/packages/aws-adapter/src/handlers/connect.ts +44 -0
- package/packages/aws-adapter/src/handlers/default.ts +322 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +29 -0
- package/packages/aws-adapter/src/handlers/index.ts +248 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
- package/packages/aws-adapter/src/handlers/state.ts +13 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
- package/packages/aws-adapter/src/index.ts +67 -0
- package/packages/aws-adapter/src/message-store.ts +34 -0
- package/packages/aws-adapter/src/serialize.ts +283 -0
- package/packages/aws-adapter/src/tables.ts +53 -0
- package/packages/aws-adapter/tests/aws-harness.ts +408 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +158 -0
- package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
- package/packages/aws-adapter/tests/global-setup.ts +301 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
- package/packages/aws-adapter/tests/handlers.test.ts +427 -0
- package/packages/aws-adapter/tests/message-store.test.ts +55 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
- package/packages/aws-adapter/tests/serialize.test.ts +249 -0
- package/packages/aws-adapter/tests/smoke.test.ts +48 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
- package/packages/aws-adapter/tests/tables.test.ts +74 -0
- package/packages/aws-adapter/tests/transactions.test.ts +446 -0
- package/packages/aws-adapter/tsconfig.build.json +16 -0
- package/packages/aws-adapter/tsconfig.test.json +14 -0
- package/packages/aws-adapter/vitest.config.ts +23 -0
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/config-loader.ts +106 -0
- package/packages/cf-adapter/src/connection-do.ts +34 -0
- package/packages/cf-adapter/tests/cf-harness.ts +2 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +42 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +108 -3
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +115 -0
- package/packages/irc-core/package.json +12 -2
- package/packages/irc-core/src/admission.ts +216 -0
- package/packages/irc-core/src/caps/capabilities.ts +1 -0
- package/packages/irc-core/src/cloak.ts +81 -0
- package/packages/irc-core/src/commands/cap.ts +1 -2
- package/packages/irc-core/src/commands/chathistory.ts +305 -0
- package/packages/irc-core/src/commands/index.ts +7 -0
- package/packages/irc-core/src/commands/join.ts +59 -7
- package/packages/irc-core/src/commands/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +71 -14
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/config.ts +247 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +5 -0
- package/packages/irc-core/src/ports.ts +655 -0
- package/packages/irc-core/src/protocol/base64.ts +16 -0
- package/packages/irc-core/src/protocol/index.ts +2 -1
- package/packages/irc-core/src/protocol/numerics.ts +11 -0
- package/packages/irc-core/src/protocol/parser.ts +27 -2
- package/packages/irc-core/src/state/connection.ts +41 -0
- package/packages/irc-core/src/types.ts +66 -2
- package/packages/irc-core/stryker.commands.conf.json +41 -0
- package/packages/irc-core/stryker.protocol.conf.json +26 -0
- package/packages/irc-core/tests/admission.test.ts +229 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
- package/packages/irc-core/tests/cloak.test.ts +78 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
- package/packages/irc-core/tests/commands/join.test.ts +246 -2
- package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +277 -9
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/config.test.ts +646 -0
- package/packages/irc-core/tests/flood-control.test.ts +394 -0
- package/packages/irc-core/tests/message-store.test.ts +530 -0
- package/packages/irc-core/tests/parser.test.ts +44 -1
- package/packages/irc-core/tests/ports.test.ts +263 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +162 -44
- package/packages/irc-server/src/dispatch.ts +89 -5
- package/packages/irc-server/src/index.ts +2 -0
- package/packages/irc-server/src/routing.ts +151 -0
- package/packages/irc-server/tests/actor.test.ts +470 -14
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +201 -0
- package/packages/irc-test-support/README.md +32 -0
- package/packages/irc-test-support/package.json +37 -0
- package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +1 -1
- package/packages/irc-test-support/src/index.ts +24 -0
- package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
- package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
- package/packages/irc-test-support/tests/smoke.test.ts +11 -0
- package/packages/irc-test-support/tsconfig.build.json +16 -0
- package/packages/irc-test-support/tsconfig.test.json +14 -0
- package/packages/irc-test-support/vitest.config.ts +20 -0
- package/progress.md +107 -0
- package/tickets.md +2485 -0
- package/tools/ci-hardening/package.json +30 -0
- package/tools/ci-hardening/src/index.ts +6 -0
- package/tools/ci-hardening/src/validate.ts +103 -0
- package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
- package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
- package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
- package/tools/ci-hardening/tests/validate.test.ts +177 -0
- package/tools/ci-hardening/tsconfig.build.json +12 -0
- package/tools/ci-hardening/tsconfig.test.json +10 -0
- package/tools/ci-hardening/vitest.config.ts +21 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
- package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
- package/tools/tcp-ws-forwarder/src/main.ts +33 -14
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
- package/webircgateway/LICENSE +201 -0
- package/webircgateway/Makefile +44 -0
- package/webircgateway/README.md +134 -0
- package/webircgateway/config.conf.example +135 -0
- package/webircgateway/go.mod +16 -0
- package/webircgateway/go.sum +89 -0
- package/webircgateway/main.go +118 -0
- package/webircgateway/pkg/dnsbl/dnsbl.go +121 -0
- package/webircgateway/pkg/identd/identd.go +86 -0
- package/webircgateway/pkg/identd/rpcclient.go +59 -0
- package/webircgateway/pkg/irc/isupport.go +56 -0
- package/webircgateway/pkg/irc/message.go +217 -0
- package/webircgateway/pkg/irc/state.go +79 -0
- package/webircgateway/pkg/proxy/proxy.go +129 -0
- package/webircgateway/pkg/proxy/server.go +237 -0
- package/webircgateway/pkg/recaptcha/recaptcha.go +59 -0
- package/webircgateway/pkg/webircgateway/client.go +741 -0
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +495 -0
- package/webircgateway/pkg/webircgateway/config.go +385 -0
- package/webircgateway/pkg/webircgateway/gateway.go +278 -0
- package/webircgateway/pkg/webircgateway/gateway_utils.go +133 -0
- package/webircgateway/pkg/webircgateway/hooks.go +152 -0
- package/webircgateway/pkg/webircgateway/letsencrypt.go +41 -0
- package/webircgateway/pkg/webircgateway/messagetags.go +103 -0
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +206 -0
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +107 -0
- package/webircgateway/pkg/webircgateway/transport_tcp.go +113 -0
- package/webircgateway/pkg/webircgateway/transport_websocket.go +126 -0
- package/webircgateway/pkg/webircgateway/utils.go +147 -0
- package/webircgateway/plugins/example/plugin.go +11 -0
- package/webircgateway/plugins/stats/plugin.go +52 -0
- package/webircgateway/staticcheck.conf +1 -0
- package/webircgateway/webircgateway.svg +3 -0
- package/packages/cf-adapter/tests/integration/index.ts +0 -17
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
package/tickets.md
ADDED
|
@@ -0,0 +1,2485 @@
|
|
|
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**.*
|