serverless-ircd 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/deploy-aws.yml +1 -3
  3. package/.github/workflows/deploy-cf-tcp.yml +87 -0
  4. package/.github/workflows/deploy-cf.yml +1 -1
  5. package/.node-version +1 -0
  6. package/.nvmrc +1 -0
  7. package/CHANGELOG.md +258 -18
  8. package/README.md +72 -30
  9. package/apps/aws-stack/README.md +2 -2
  10. package/apps/aws-stack/bin/aws.ts +7 -0
  11. package/apps/aws-stack/package.json +4 -4
  12. package/apps/aws-stack/src/aws-stack.ts +118 -6
  13. package/apps/aws-stack/tests/stack.test.ts +98 -3
  14. package/apps/cf-tcp-container/Dockerfile +69 -0
  15. package/apps/cf-tcp-container/package.json +34 -0
  16. package/apps/cf-tcp-container/src/config-loader.ts +145 -0
  17. package/apps/cf-tcp-container/src/container-do.ts +38 -0
  18. package/apps/cf-tcp-container/src/container-server.ts +363 -0
  19. package/apps/cf-tcp-container/src/main.ts +77 -0
  20. package/apps/cf-tcp-container/src/persistence.ts +144 -0
  21. package/apps/cf-tcp-container/src/worker.ts +41 -0
  22. package/apps/cf-tcp-container/terraform/provider.tf +24 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
  25. package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
  26. package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
  27. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
  28. package/apps/cf-tcp-container/tsconfig.build.json +17 -0
  29. package/apps/cf-tcp-container/tsconfig.test.json +15 -0
  30. package/apps/cf-tcp-container/vitest.config.ts +26 -0
  31. package/apps/cf-tcp-container/wrangler.toml +63 -0
  32. package/apps/cf-worker/package.json +1 -1
  33. package/apps/cf-worker/scripts/smoke.mjs +0 -0
  34. package/apps/cf-worker/src/worker.ts +8 -2
  35. package/apps/cf-worker/tests/smoke.test.ts +1 -0
  36. package/apps/cf-worker/wrangler.test.toml +11 -1
  37. package/apps/cf-worker/wrangler.toml +69 -19
  38. package/apps/local-cli/package.json +1 -1
  39. package/apps/local-cli/src/config-loader.ts +11 -0
  40. package/apps/local-cli/src/main.ts +1 -1
  41. package/apps/local-cli/src/server.ts +46 -3
  42. package/apps/local-cli/tests/e2e.test.ts +52 -0
  43. package/package.json +19 -16
  44. package/packages/aws-adapter/package.json +3 -3
  45. package/packages/aws-adapter/src/account-store.ts +121 -0
  46. package/packages/aws-adapter/src/admission.ts +74 -0
  47. package/packages/aws-adapter/src/aws-runtime.ts +124 -34
  48. package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
  49. package/packages/aws-adapter/src/config-loader.ts +62 -0
  50. package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
  51. package/packages/aws-adapter/src/handlers/connect.ts +64 -8
  52. package/packages/aws-adapter/src/handlers/default.ts +43 -2
  53. package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
  54. package/packages/aws-adapter/src/handlers/index.ts +120 -9
  55. package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
  56. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  57. package/packages/aws-adapter/src/index.ts +13 -0
  58. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
  59. package/packages/aws-adapter/tests/account-store.test.ts +279 -0
  60. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  61. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  62. package/packages/aws-adapter/tests/config-loader.test.ts +75 -0
  63. package/packages/aws-adapter/tests/connect.test.ts +78 -0
  64. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
  65. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  66. package/packages/aws-adapter/tests/handlers.test.ts +194 -47
  67. package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
  68. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  69. package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
  70. package/packages/aws-adapter/tests/transactions.test.ts +25 -20
  71. package/packages/cf-adapter/package.json +1 -1
  72. package/packages/cf-adapter/src/cf-runtime.ts +40 -22
  73. package/packages/cf-adapter/src/channel-do.ts +40 -1
  74. package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
  75. package/packages/cf-adapter/src/config-loader.ts +62 -0
  76. package/packages/cf-adapter/src/connection-do.ts +181 -31
  77. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  78. package/packages/cf-adapter/src/env.ts +58 -8
  79. package/packages/cf-adapter/src/index.ts +11 -9
  80. package/packages/cf-adapter/tests/cf-harness.ts +11 -1
  81. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
  82. package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
  83. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  84. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  85. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  86. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  87. package/packages/cf-adapter/tests/connection-do.test.ts +40 -0
  88. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  89. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  90. package/packages/cf-adapter/tests/worker/main.ts +9 -1
  91. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
  92. package/packages/cf-adapter/wrangler.test.toml +18 -1
  93. package/packages/in-memory-runtime/package.json +1 -1
  94. package/packages/in-memory-runtime/src/in-memory-runtime.ts +14 -28
  95. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
  96. package/packages/irc-core/package.json +8 -2
  97. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  98. package/packages/irc-core/src/caps/capabilities.ts +23 -2
  99. package/packages/irc-core/src/case-fold.ts +64 -0
  100. package/packages/irc-core/src/cloak.ts +1 -1
  101. package/packages/irc-core/src/commands/cap.ts +8 -1
  102. package/packages/irc-core/src/commands/index.ts +11 -0
  103. package/packages/irc-core/src/commands/invite.ts +6 -9
  104. package/packages/irc-core/src/commands/ison.ts +61 -0
  105. package/packages/irc-core/src/commands/isupport.ts +1 -1
  106. package/packages/irc-core/src/commands/join.ts +4 -3
  107. package/packages/irc-core/src/commands/kick.ts +4 -11
  108. package/packages/irc-core/src/commands/list.ts +5 -4
  109. package/packages/irc-core/src/commands/mode.ts +5 -9
  110. package/packages/irc-core/src/commands/motd-lines.ts +127 -0
  111. package/packages/irc-core/src/commands/motd.ts +6 -110
  112. package/packages/irc-core/src/commands/oper.ts +151 -0
  113. package/packages/irc-core/src/commands/quit.ts +12 -0
  114. package/packages/irc-core/src/commands/registration.ts +26 -19
  115. package/packages/irc-core/src/commands/sasl.ts +72 -9
  116. package/packages/irc-core/src/commands/server-info.ts +129 -0
  117. package/packages/irc-core/src/commands/tagmsg.ts +205 -0
  118. package/packages/irc-core/src/commands/userhost.ts +84 -0
  119. package/packages/irc-core/src/commands/whowas.ts +113 -0
  120. package/packages/irc-core/src/config.ts +84 -3
  121. package/packages/irc-core/src/credential-hashing.ts +124 -0
  122. package/packages/irc-core/src/effects.ts +6 -29
  123. package/packages/irc-core/src/index.ts +3 -0
  124. package/packages/irc-core/src/ports.ts +240 -7
  125. package/packages/irc-core/src/protocol/numerics.ts +14 -8
  126. package/packages/irc-core/src/types.ts +60 -1
  127. package/packages/irc-core/tests/account-store.test.ts +131 -0
  128. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  129. package/packages/irc-core/tests/case-fold.test.ts +80 -0
  130. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  131. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  132. package/packages/irc-core/tests/commands/kick.test.ts +15 -0
  133. package/packages/irc-core/tests/commands/oper.test.ts +257 -0
  134. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  135. package/packages/irc-core/tests/commands/registration.test.ts +256 -19
  136. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  137. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  138. package/packages/irc-core/tests/commands/tagmsg.test.ts +662 -0
  139. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  140. package/packages/irc-core/tests/commands/who.test.ts +22 -4
  141. package/packages/irc-core/tests/commands/whois.test.ts +8 -1
  142. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  143. package/packages/irc-core/tests/config.test.ts +170 -1
  144. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  145. package/packages/irc-core/tests/effects.test.ts +0 -27
  146. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  147. package/packages/irc-core/tests/numerics.test.ts +12 -0
  148. package/packages/irc-core/tests/types.test.ts +35 -1
  149. package/packages/irc-core/tsconfig.build.json +1 -1
  150. package/packages/irc-core/tsconfig.test.json +1 -1
  151. package/packages/irc-server/package.json +1 -1
  152. package/packages/irc-server/src/actor.ts +182 -14
  153. package/packages/irc-server/src/dispatch.ts +0 -3
  154. package/packages/irc-server/src/index.ts +10 -2
  155. package/packages/irc-server/src/routing.ts +21 -0
  156. package/packages/irc-server/src/transport.ts +101 -0
  157. package/packages/irc-server/tests/actor.test.ts +617 -1
  158. package/packages/irc-server/tests/dispatch.test.ts +0 -17
  159. package/packages/irc-server/tests/routing.test.ts +7 -0
  160. package/packages/irc-server/tests/transport.test.ts +230 -0
  161. package/packages/irc-test-support/package.json +1 -1
  162. package/packages/irc-test-support/src/harness.ts +44 -9
  163. package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
  164. package/packages/irc-test-support/src/index.ts +3 -0
  165. package/packages/irc-test-support/src/scenarios.ts +132 -2
  166. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  167. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  168. package/pnpm-workspace.yaml +9 -0
  169. package/tools/ci-hardening/package.json +1 -1
  170. package/tools/package.json +4 -0
  171. package/tools/seed-aws-accounts.ts +79 -0
  172. package/tools/seed-cf-accounts.ts +104 -0
  173. package/tools/tcp-ws-forwarder/package.json +1 -1
  174. package/AGENTS.md +0 -5
  175. package/MOTD.txt +0 -3
  176. package/PLAN-FIXES.md +0 -358
  177. package/PLAN.md +0 -420
  178. package/dashboards/cloudwatch-irc.json +0 -139
  179. package/docs/AWS-Adapter-Architecture.md +0 -494
  180. package/docs/AWS-Deployment.md +0 -1107
  181. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  182. package/docs/Home.md +0 -1
  183. package/docs/Observability.md +0 -87
  184. package/docs/PlanIRCv3Websocket.md +0 -489
  185. package/docs/PlanWebClient.md +0 -451
  186. package/docs/Release-Process.md +0 -440
  187. package/progress.md +0 -107
  188. package/tickets.md +0 -2485
  189. package/webircgateway/LICENSE +0 -201
  190. package/webircgateway/Makefile +0 -44
  191. package/webircgateway/README.md +0 -134
  192. package/webircgateway/config.conf.example +0 -135
  193. package/webircgateway/go.mod +0 -16
  194. package/webircgateway/go.sum +0 -89
  195. package/webircgateway/main.go +0 -118
  196. package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
  197. package/webircgateway/pkg/identd/identd.go +0 -86
  198. package/webircgateway/pkg/identd/rpcclient.go +0 -59
  199. package/webircgateway/pkg/irc/isupport.go +0 -56
  200. package/webircgateway/pkg/irc/message.go +0 -217
  201. package/webircgateway/pkg/irc/state.go +0 -79
  202. package/webircgateway/pkg/proxy/proxy.go +0 -129
  203. package/webircgateway/pkg/proxy/server.go +0 -237
  204. package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
  205. package/webircgateway/pkg/webircgateway/client.go +0 -741
  206. package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
  207. package/webircgateway/pkg/webircgateway/config.go +0 -385
  208. package/webircgateway/pkg/webircgateway/gateway.go +0 -278
  209. package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
  210. package/webircgateway/pkg/webircgateway/hooks.go +0 -152
  211. package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
  212. package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
  213. package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
  214. package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
  215. package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
  216. package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
  217. package/webircgateway/pkg/webircgateway/utils.go +0 -147
  218. package/webircgateway/plugins/example/plugin.go +0 -11
  219. package/webircgateway/plugins/stats/plugin.go +0 -52
  220. package/webircgateway/staticcheck.conf +0 -1
  221. package/webircgateway/webircgateway.svg +0 -3
package/PLAN.md DELETED
@@ -1,420 +0,0 @@
1
- # ServerlessIRCd — Project Plan
2
-
3
- A serverless IRC daemon: the IRC protocol logic is a pure, platform-agnostic
4
- core; two thin adapters run it on **Cloudflare Workers** (Durable Objects) or
5
- **AWS** (API Gateway WebSockets + Lambda + DynamoDB).
6
-
7
- Single logical server (no S2S linking in v1). Practical modern IRC subset plus
8
- selected IRCv3 extensions. Production-ish quality, strict TDD.
9
-
10
- ---
11
-
12
- ## 1. Goals & non-goals
13
-
14
- **Goals**
15
- - One TypeScript codebase implementing IRC server semantics, testable with zero
16
- cloud dependencies.
17
- - Two deployable adapters (CF Workers, AWS) sharing that core.
18
- - Works with modern clients (WeeChat, HexChat, IRCCloud, TheLounge).
19
- - ≥90% coverage on core; 100% on protocol/parsing and command reducers.
20
-
21
- **Non-goals (v1)**
22
- - Server-to-server linking / federated networks (TS6, IRCv3 S2S).
23
- - Full RFC 2812 legacy compliance.
24
- - Services (NickServ/ChanServ) — designed for, not built.
25
- - Operator server-mesh features (connect/jupe/squit routing).
26
-
27
- **Design-for-later**
28
- - A `ServerLink` seam in the core so S2S can be added without rewriting handlers.
29
-
30
- ---
31
-
32
- ## 2. Architecture
33
-
34
- Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
35
-
36
- ```
37
- ┌─────────────────────────────────────────────────────────────────┐
38
- │ packages/irc-core (pure TS, no cloud deps) │
39
- │ protocol/ parse · serialize · numerics · messages │
40
- │ commands/ pure reducers: (state, msg) → { state, effects } │
41
- │ state/ channel · connection · mode · registry shapes │
42
- │ caps/ IRCv3 capability negotiation table │
43
- │ isupport/ 005 generation │
44
- └─────────────────────────────────────────────────────────────────┘
45
- ▲ implements IrcRuntime (port)
46
-
47
- ┌─────────────────────────────────────────────────────────────────┐
48
- │ packages/irc-server (orchestration, defines the port) │
49
- │ IrcRuntime side-effect interface (transport + state IO) │
50
- │ ConnectionActor bytes → parse → reducer → runtime │
51
- │ location table which authority owns each command │
52
- └─────────────────────────────────────────────────────────────────┘
53
- ▲ ▲
54
- ┌────────────────────────────┐ ┌──────────────────────────────┐
55
- │ adapters/cf │ │ adapters/aws │
56
- │ ConnectionDO │ │ $connect/$disconnect/ │
57
- │ ChannelDO │ │ $default Lambda handlers │
58
- │ RegistryDO │ │ DynamoDB tables │
59
- │ CfRuntime │ │ AwsRuntime │
60
- │ wrangler.toml │ │ CDK stack │
61
- └────────────────────────────┘ └──────────────────────────────┘
62
- ▲ ▲
63
- ┌─────────────────────────────────────────────────────────────────┐
64
- │ apps/cf-worker apps/aws-stack (thin deploy glue) │
65
- └─────────────────────────────────────────────────────────────────┘
66
-
67
- packages/in-memory-runtime ← reference impl of IrcRuntime, used by
68
- integration tests and a local CLI server
69
- ```
70
-
71
- ### 2.1 The key idea: pure reducers + location-of-authority
72
-
73
- Each command handler is a **pure function**:
74
-
75
- ```ts
76
- type Reducer<S> = (state: S, msg: IrcMessage, ctx: Ctx) => { state: S; effects: Effect[] };
77
- ```
78
-
79
- Side effects are values (`Effect[]`), not performed in the handler. This makes
80
- every handler a trivial unit test: arrange state, apply message, assert state'
81
- and emitted effects.
82
-
83
- **Where does each reducer run?** Whichever authority owns the state it mutates.
84
- This avoids cross-instance read-modify-write races:
85
-
86
- | Command family | Runs in authority | Primary state mutated |
87
- |-----------------------------|-------------------------|------------------------------|
88
- | Registration (NICK/USER) | Connection entity | Connection state |
89
- | PING/PONG, QUIT, AWAY | Connection entity | Connection state |
90
- | User MODE, SETNAME | Connection entity | Connection state |
91
- | JOIN / PART / channel MODE | Channel entity | Roster, channel modes |
92
- | PRIVMSG/NOTICE to channel | Channel entity | Fanout (reads roster) |
93
- | TOPIC, KICK, INVITE | Channel entity | Channel state / roster |
94
- | PRIVMSG/NOTICE to user | Sender Connection | (uses Nick→Connection route) |
95
- | Nick collision check | Registry entity | Nick→Connection map |
96
- | NAMES / WHO / WHOIS | Reads snapshots | (no mutation) |
97
-
98
- Cross-authority coordination (e.g., JOIN must update the connection's joined
99
- list **and** the channel roster) is done by the authority that accepts the
100
- action emitting an effect the other authority consumes. On CF this is a stub()
101
- call between DOs; on AWS a `TransactWriteItems` plus EventBridge fanout.
102
-
103
- ### 2.2 The `IrcRuntime` port (side-effect interface)
104
-
105
- The set of capabilities a reducer's emitted effects require. The in-memory,
106
- CF, and AWS runtimes all implement it:
107
-
108
- ```ts
109
- interface IrcRuntime {
110
- // transport
111
- send(to: ConnId, lines: RawLine[]): Promise<void>;
112
- broadcast(chan: ChanName, lines: RawLine[], except?: ConnId): Promise<void>;
113
- disconnect(conn: ConnId, reason?: string): Promise<void>;
114
-
115
- // nickname registry
116
- reserveNick(nick: Nick, conn: ConnId): Promise<{ ok: true } | { ok: false }>;
117
- changeNick(conn: ConnId, oldNick: Nick, newNick: Nick): Promise<boolean>;
118
- releaseNick(nick: Nick): Promise<void>;
119
-
120
- // channel ownership
121
- applyChannelDelta(chan: ChanName, delta: ChannelDelta): Promise<void>;
122
-
123
- // lookups (NAMES, WHOIS, routing)
124
- lookupNick(nick: Nick): Promise<ConnId | null>;
125
- getConnectionInfo(conn: ConnId): Promise<ConnSnapshot | null>;
126
- getChannelSnapshot(chan: ChanName): Promise<ChanSnapshot | null>;
127
- }
128
- ```
129
-
130
- `Effect[]` values are interpreted by a single `dispatch(effects, runtime)`
131
- function in `irc-server`. The reducer stays pure; only `dispatch` awaits.
132
-
133
- ---
134
-
135
- ## 3. Protocol scope (v1)
136
-
137
- **Core (RFC 1459/2812 subset)**
138
- - Registration: `NICK`, `USER`, `CAP` negotiation, `PASS` (server password)
139
- - `PING` / `PONG` (server-initiated keepalive + client-initiated)
140
- - `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`
141
- - `MODE` (user + channel), `TOPIC`, `KICK`, `INVITE`
142
- - `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD`
143
- - Numeric replies: `001`–`005`, `311`–`319`, `321`–`323`, `331`/`332`,
144
- `341`, `352`/`315`, `353`/`366`, `367`–`369`, `372`/`375`/`376`,
145
- `401`–`407`, `421`, `432`–`437`, `441`–`449`, `461`–`473`, `482`,
146
- `501`/`502`, errors as needed.
147
-
148
- **Channel modes**: `o v b i k l t n m s p` (op, voice, ban, invite-only, key,
149
- limit, topic-lock, no-external, moderated, secret, private).
150
- **User modes**: `i` (invisible), `o` (oper — local only), `w`, `s`.
151
-
152
- **IRCv3 extensions (negotiated via `CAP`)**
153
- - `message-tags` (`+tag`), `server-time` (`time`), `account-tag`
154
- - `echo-message`
155
- - `batch`
156
- - `sasl` (mechanisms: `PLAIN`, `EXTERNAL` reserved for later with mTLS)
157
- - `multi-prefix`
158
- - `away-notify`, `chghost`
159
- - `invite-notify`, `extended-join`
160
-
161
- **Connection framing**: WebSockets carry one IRC message per text frame
162
- (ws text frames are IRC-line-delimited; we also tolerate `\r\n`-joined frames).
163
- No plaintext TCP in v1 (wss only) — keeps serverless story clean.
164
-
165
- ---
166
-
167
- ## 4. State model & authoritative ownership
168
-
169
- | State | Authority (CF / AWS) | Keyed by |
170
- |-----------------------------|---------------------------------------------|--------------------|
171
- | Connection (nick,user,hostmask,caps,away,joined list) | ConnectionDO / DynamoDB `Connections` | `connectionId` |
172
- | Channel (topic,modes,key,limit,ban masks) | ChannelDO / DynamoDB `ChannelMeta` | lowercased name |
173
- | Membership (members + prefixes) | ChannelDO / DynamoDB `ChannelMembers` | `channelId` (PK), `connectionId` (SK) |
174
- | Nick registry (nick → connectionId) | RegistryDO / DynamoDB `Nicks` | lowercased nick |
175
- | Authenticated account (SASL) | Shared secret store / DynamoDB `Accounts` | account name |
176
-
177
- **Invariant**: nickname uniqueness and channel membership are enforced with
178
- conditional writes (DynamoDB `ConditionExpression`) or DO single-threaded
179
- serial execution — there is exactly one authority per piece of state, so races
180
- are structurally impossible rather than defended against.
181
-
182
- ---
183
-
184
- ## 5. Repository layout
185
-
186
- ```
187
- ServerlessIRCd/
188
- ├── packages/
189
- │ ├── irc-core/ pure protocol + reducers (the brain)
190
- │ │ ├── src/
191
- │ │ │ ├── protocol/ parser.ts serializer.ts numerics.ts messages.ts
192
- │ │ │ ├── commands/ registration.ts join.ts part.ts privmsg.ts
193
- │ │ │ │ mode.ts topic.ts kick.ts ping.ts quit.ts names.ts
194
- │ │ │ │ who.ts whois.ts cap.ts sasl.ts list.ts motd.ts
195
- │ │ │ ├── state/ connection.ts channel.ts mode.ts registry.ts
196
- │ │ │ ├── caps/ capabilities.ts
197
- │ │ │ ├── isupport/ isupport.ts
198
- │ │ │ ├── effects.ts types.ts ids.ts clock.ts (ports)
199
- │ │ │ └── index.ts
200
- │ │ └── tests/ (unit, golden vectors)
201
- │ ├── irc-server/ orchestration, IrcRuntime port, dispatch
202
- │ │ ├── src/{runtime.ts, actor.ts, dispatch.ts, location.ts}
203
- │ │ └── tests/
204
- │ ├── in-memory-runtime/ reference runtime + local CLI server
205
- │ ├── cf-adapter/ Durable Objects + CfRuntime
206
- │ │ ├── src/{connection-do.ts channel-do.ts registry-do.ts runtime.ts env.ts}
207
- │ │ ├── tests/ (vitest-pool-workers / Miniflare)
208
- │ │ └── wrangler.toml
209
- │ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime
210
- │ ├── src/{handlers runtime tables.ts}
211
- │ ├── tests/ (dynamodb-local / localstack)
212
- │ └── cdk/ (infra stack)
213
- ├── apps/
214
- │ ├── cf-worker/ worker entry, DO migrations, bindings
215
- │ ├── aws-stack/ CDK app entry
216
- │ └── local-cli/ runnable server using in-memory-runtime
217
- ├── tools/
218
- │ ├── irc-client/ minimal test client for e2e
219
- │ └── load-test/ synthetic client fan-out
220
- ├── tests/
221
- │ ├── integration/ protocol scenarios via IrcRuntime contract
222
- │ ├── e2e/ real WebSocket clients vs deployed adapter
223
- │ └── fixtures/ golden messages, capture scripts
224
- ├── docs/
225
- │ ├── architecture.md protocol-scope.md deployment-cf.md deployment-aws.md
226
- │ └── adr/ architecture decision records
227
- ├── pnpm-workspace.yaml turbo.json tsconfig.base.json
228
- ├── biome.json (or .eslintrc + .prettierrc) vitest.config.ts
229
- ├── .github/workflows/ci.yml
230
- └── PLAN.md AGENTS.md README.md
231
- ```
232
-
233
- ---
234
-
235
- ## 6. Platform mapping
236
-
237
- ### 6.1 Cloudflare Workers
238
-
239
- - **Edge worker**: WebSocket upgrade entry; routes each connection to its
240
- `ConnectionDO` (`idFromName(connectionId)`). Cheap hibernatable WS via
241
- `HibernatingWebSocketHandler` keeps idle connections free.
242
- - **`ConnectionDO`**: owns one client's socket + connection state; runs
243
- connection-scoped reducers; on QUIT/PART/JOIN issues stub() calls into the
244
- relevant `ChannelDO`(s) and `RegistryDO`.
245
- - **`ChannelDO`** (keyed by lowercased channel): serial execution → authoritative
246
- roster + channel modes + fanout. Broadcast = loop members, call each
247
- `ConnectionDO.fetch()`/`webSocket.send` via stored routing tag.
248
- - **`RegistryDO`** (sharded by `hash(nick) % N` for hot-nick scaling): nickname
249
- uniqueness + nick→connectionId map. Conditional reserve via single-threaded DO.
250
- - **Persistence**: `state.storage` (DO transactional storage) for durable state;
251
- optional `D1`/`KV` for MOTD, config, accounts; `Durable Object alarms` for
252
- PING/idle timeouts.
253
- - **Tests**: `@cloudflare/vitest-pool-workers` against real workerd.
254
-
255
- ### 6.2 AWS
256
-
257
- - **API Gateway WebSocket API**: `$connect`, `$disconnect`, `$default` → Lambda
258
- (Node 20). The `connectionId` is the stable connection identity; send via
259
- `ApiGatewayManagementApi.postToConnection`.
260
- - **DynamoDB**:
261
- - `Connections` (PK `connectionId`): nick, user, hostmask, caps, joined
262
- channels, idle since, callback endpoint.
263
- - `ChannelMeta` (PK `channelName`): topic, modes, key, limit.
264
- - `ChannelMembers` (PK `channelName`, SK `connectionId`): prefixes.
265
- - `Nicks` (PK `nickLower`): `connectionId` (unique via conditional PutItem).
266
- - `Accounts` (PK `account`): SASL credentials (never plaintext — store hash).
267
- - **Transactions**: `TransactWriteItems` for membership changes that span
268
- Connections ↔ ChannelMembers.
269
- - **Fanout**: `$default` Lambda loads the channel's members (query), then
270
- `postToConnection` per member; failures (GoneException) mark the connection
271
- disconnected and trigger cleanup (lazy on next touch + a sweeper).
272
- - **Timers**: EventBridge Scheduler for idle/PING sweeps; per-connection TTLs.
273
- - **Infra**: **AWS CDK v2** (TypeScript). dynamodb-local + localstack for tests.
274
-
275
- ### 6.3 Matrix: same runtime contract, different substrates
276
-
277
- | Capability | CF adapter | AWS adapter |
278
- |--------------------|-------------------------------------|------------------------------------------|
279
- | Connection identity| DO name from connectionId | APIGW connectionId |
280
- | Long-lived socket | Hibernatable WebSocket in DO | APIGW persistent WS (≤2h) |
281
- | Send to client | `ws.send` in target ConnectionDO | `ApiGatewayManagementApi.postToConnection`|
282
- | Channel authority | ChannelDO (single-threaded) | DynamoDB `ChannelMembers` + transactions |
283
- | Nick uniqueness | RegistryDO | DynamoDB conditional Put on `Nicks` |
284
- | Cross-instance pub/sub | stub() DO→DO | DynamoDB query + PostToConnection fanout |
285
- | Timers/idle | DO alarms | EventBridge Scheduler |
286
- | Durable storage | DO transactional storage / D1 | DynamoDB |
287
- | Secret config | Worker secrets / KV | Secrets Manager / SSM |
288
-
289
- ---
290
-
291
- ## 7. Roadmap (TDD: Red → Green → Refactor per item)
292
-
293
- Each task below starts with a failing test. Phases are shippable milestones.
294
-
295
- ### Phase 0 — Foundation
296
- 1. Monorepo: pnpm workspaces + TS project references + turbo.
297
- 2. `vitest`, coverage (c8/v8), `biome`, CI skeleton (typecheck/lint/test/coverage gate).
298
- 3. `irc-core/protocol`: message types, numeric registry, parser golden vectors.
299
- 4. `irc-core/protocol`: serializer with round-trip property tests.
300
-
301
- ### Phase 1 — Pure protocol engine (no cloud)
302
- 5. State shapes + `Effect` taxonomy + `dispatch` interpreter.
303
- 6. Reducers (one file/PR each, test-first): `PING/PONG`, `QUIT`, registration
304
- (`NICK`/`USER`/`CAP`/`PASS`), `JOIN`, `PART`, `PRIVMSG`/`NOTICE` (channel +
305
- private), `TOPIC`, `NAMES`, `MOTD`, `MODE` (channel + user), `KICK`,
306
- `INVITE`, `WHO`, `WHOIS`, `LIST`.
307
- 7. IRCv3: `CAP LS/REQ/ACK/NAK/END`, `message-tags`, `server-time`,
308
- `echo-message`, `multi-prefix`, `extended-join`, `chghost`, `away-notify`,
309
- `invite-notify`, `batch`.
310
- 8. SASL: `PLAIN` auth via pluggable `AccountStore` port; `EXTERNAL` stubbed.
311
- 9. isupport (`005`) generator from server config.
312
- 10. Flood control (token bucket) as a pure reducer wrapper.
313
-
314
- ### Phase 2 — Runtime port + reference impl
315
- 11. Define `IrcRuntime`; implement `in-memory-runtime`.
316
- 12. `ConnectionActor`: bytes → framed messages → reducer → dispatch.
317
- 13. `apps/local-cli`: a runnable WebSocket server using in-memory runtime
318
- (great for manual testing and e2e fixtures).
319
- 14. Integration test suite: scenarios expressed once, runnable against any
320
- `IrcRuntime` (parametrized: in-memory, then CF, then AWS).
321
-
322
- ### Phase 3 — Cloudflare adapter
323
- 15. `ConnectionDO` (hibernatable WS), state migration, alarms for PING/idle.
324
- 16. `RegistryDO` (with sharding plan).
325
- 17. `ChannelDO` (roster + modes + fanout via ConnectionDO stubs).
326
- 18. `CfRuntime` implementing `IrcRuntime`.
327
- 19. Deploy pipeline (wrangler), staging env, smoke e2e.
328
- 20. Docs: `deployment-cf.md`.
329
-
330
- ### Phase 4 — AWS adapter
331
- 21. CDK stack: APIGW WebSocket, Lambda, DynamoDB tables, IAM.
332
- 22. `$connect`/`$disconnect`/`$default` handlers; `AwsRuntime`.
333
- 23. Transactions for membership; gone-connection sweep.
334
- 24. EventBridge idle/PING timers.
335
- 25. Deploy pipeline (CDK), staging env, smoke e2e.
336
- 26. Docs: `deployment-aws.md`.
337
-
338
- ### Phase 5 — Cross-cutting
339
- 27. Observability: structured logs, trace IDs, CF analytics / CloudWatch dashboards.
340
- 28. Security: server password, SASL, hostmask cloaking, rate limits, max
341
- channels/connections per user, input length caps (512-byte line per spec).
342
- 29. Config: MOTD, server name, network name, allowed channel prefixes, max
343
- clients, oper credentials.
344
- 30. CI hardening: coverage gate ≥90%, contract tests run on every PR, mutation
345
- testing (e.g., stryker) spot-check on core.
346
-
347
- ### Phase 6 — Hardening / polish
348
- 31. Load test: 10k concurrent connections per platform; capture bottlenecks.
349
- 32. Compatibility sweep: WeeChat, HexChat, IRCCloud, TheLounge, matrix-IRC bridge.
350
- 33. ADRs for the irreversible choices (state-authority split, effect system,
351
- DO sharding, DynamoDB schema, wss-only v1).
352
-
353
- ---
354
-
355
- ## 8. Testing strategy
356
-
357
- | Layer | Tooling | What it asserts |
358
- |------------------|--------------------------------------------|---------------------------------------|
359
- | Parser/serializer| vitest + property tests (fast-check) | Grammar correctness, round-trip |
360
- | Reducers (core) | vitest, pure unit tests | `(state,msg) → {state,effects}` exact |
361
- | Runtime contract | vitest parametrized over `IrcRuntime` | Same scenarios pass in-memory+CF+AWS |
362
- | CF adapter | `vitest-pool-workers` (real workerd) | DO behavior, alarms, stub fanout |
363
- | AWS adapter | vitest + dynamodb-local / localstack | DynamoDB schema, transactions, fanout |
364
- | E2E | real WS clients in `tools/irc-client` | RFC-shaped flows vs deployed stack |
365
- | Load | `tools/load-test` synthetic client farm | Concurrency, latency, drop rate |
366
-
367
- **Determinism rules**: inject a `Clock` and deterministic ID/nonce ports.
368
- No real timers/random in reducers. Every test independent (fresh state).
369
-
370
- ---
371
-
372
- ## 9. Key technical decisions (proposed, to confirm)
373
-
374
- - **Monorepo**: pnpm + TypeScript project references; **turbo** for caching.
375
- - **Test runner**: **vitest** (ESM-native, fast, fits Workers).
376
- - **Lint/format**: **Biome** (single tool, fast). Fallback: ESLint + Prettier.
377
- - **Coverage**: v8/c8, gate ≥90% in CI; 100% on `irc-core`.
378
- - **CF bundler**: wrangler (esbuild internally). **AWS**: esbuild for Lambda
379
- bundles, CDK for infra.
380
- - **Runtime**: Node 20 (Lambda) and latest Workers runtime; both ES2022+.
381
- - **Framing**: wss-only, one IRC message per WebSocket text frame, also accept
382
- `\r\n`-joined frames (TolerantReader).
383
- - **Time/IDs**: injected ports (`Clock`, `IdFactory`) for deterministic tests.
384
- - **Secrets**: never logged; `hostmask cloaking` for user IPs.
385
-
386
- Open questions to resolve before Phase 3:
387
- - Channel DO sharding granularity (one DO per channel vs. bucketed)?
388
- Default: one DO per channel; revisit if hot-channel limits bite.
389
- - DO WebSocket hibernation vs. always-on (cost vs. complexity). Default:
390
- hibernation on.
391
- - AWS region strategy (single vs. multi-region with DynamoDB global tables).
392
- Default: single region for v1.
393
-
394
- ---
395
-
396
- ## 10. Risks
397
-
398
- | Risk | Mitigation |
399
- |-----------------------------------|-----------------------------------------------------------|
400
- | DO fanout latency on big channels | Batch stub calls; consider a per-DO send-list with one hop|
401
- | APIGW 2h connection cap (AWS) | Reconnect protocol; client MUST tolerate `PONG`-driven reset |
402
- | Nick race conditions | Single-authority Registry; conditional writes; no optimistic checks |
403
- | 512-byte line overflow | Hard cap in parser; split or drop per spec |
404
- | Fanout thundering herd on LIST | Throttle + cache; cap response size |
405
- | Cross-adapter drift | Parametrized contract tests over `IrcRuntime` |
406
- | Cost runaway (DynamoDB) | Single-table design where possible; RCU sizing in CDK |
407
-
408
- ---
409
-
410
- ## 11. Deliverables checklist (definition of done for v1)
411
-
412
- - [ ] `irc-core` passes full unit + property suite at 100% coverage.
413
- - [ ] `irc-server` + `in-memory-runtime` pass the parametrized contract suite.
414
- - [ ] `apps/local-cli` connects from a real client end-to-end.
415
- - [ ] `cf-adapter` deployed to staging; e2e green from ≥2 real clients.
416
- - [ ] `aws-adapter` deployed to staging; e2e green from ≥2 real clients.
417
- - [ ] Compatibility sweep passed against ≥3 reference clients.
418
- - [ ] Load test report for each platform.
419
- - [ ] ADRs written for all irreversible decisions.
420
- - [ ] README with quickstart for both platforms.
@@ -1,139 +0,0 @@
1
- {
2
- "widgets": [
3
- {
4
- "type": "metric",
5
- "x": 0,
6
- "y": 0,
7
- "width": 12,
8
- "height": 6,
9
- "properties": {
10
- "view": "timeSeries",
11
- "stacked": false,
12
- "metrics": [
13
- ["AWS/ApiGateway", "ConnectCount", "ApiId", "${API_ID}"],
14
- [".", "DisconnectCount", ".", "."]
15
- ],
16
- "region": "${AWS_REGION}",
17
- "title": "Connections per second",
18
- "period": 60,
19
- "stat": "Sum"
20
- }
21
- },
22
- {
23
- "type": "metric",
24
- "x": 12,
25
- "y": 0,
26
- "width": 12,
27
- "height": 6,
28
- "properties": {
29
- "view": "timeSeries",
30
- "stacked": false,
31
- "metrics": [
32
- [
33
- "AWS/Lambda",
34
- "Duration",
35
- "FunctionName",
36
- "${LAMBDA_NAME}",
37
- "Resource",
38
- "${LAMBDA_NAME}:$default"
39
- ]
40
- ],
41
- "region": "${AWS_REGION}",
42
- "title": "$default handler latency (p50/p95/p99)",
43
- "period": 60,
44
- "stat": ["p50", "p95", "p99"]
45
- }
46
- },
47
- {
48
- "type": "metric",
49
- "x": 0,
50
- "y": 6,
51
- "width": 12,
52
- "height": 6,
53
- "properties": {
54
- "view": "timeSeries",
55
- "stacked": false,
56
- "metrics": [["AWS/Lambda", "Errors", "FunctionName", "${LAMBDA_NAME}"]],
57
- "region": "${AWS_REGION}",
58
- "title": "Lambda errors per minute",
59
- "period": 60,
60
- "stat": "Sum"
61
- }
62
- },
63
- {
64
- "type": "log",
65
- "x": 12,
66
- "y": 6,
67
- "width": 12,
68
- "height": 6,
69
- "properties": {
70
- "view": "logs",
71
- "stacked": false,
72
- "query": "SOURCE '${LOG_GROUP_ARN}' | fields @timestamp, msg, level, traceId, connectionId, effect\n| filter level = \"error\" or msg = \"dispatch.error\" or msg = \"frame.parse-error\"\n| sort @timestamp desc\n| limit 100",
73
- "region": "${AWS_REGION}",
74
- "title": "Recent errors (level=error / dispatch.error / frame.parse-error)",
75
- "period": 60
76
- }
77
- },
78
- {
79
- "type": "log",
80
- "x": 0,
81
- "y": 12,
82
- "width": 24,
83
- "height": 6,
84
- "properties": {
85
- "view": "logs",
86
- "stacked": false,
87
- "query": "SOURCE '${LOG_GROUP_ARN}' | filter msg = \"dispatch\"\n| stats count(*) as effects_per_minute by histogram(@timestamp, 1m)\n| sort @timestamp desc",
88
- "region": "${AWS_REGION}",
89
- "title": "Dispatched effects per minute (from structured log msg=dispatch)",
90
- "period": 60
91
- }
92
- }
93
- ],
94
- "dashboardNameTemplate": "ServerlessIRCd-${ENVIRONMENT}",
95
- "dashboardFilters": {
96
- "variables": [
97
- {
98
- "name": "ENVIRONMENT",
99
- "type": "Pattern",
100
- "value": "staging",
101
- "input": {
102
- "id": "input/text"
103
- }
104
- },
105
- {
106
- "name": "API_ID",
107
- "type": "Pattern",
108
- "value": "",
109
- "input": {
110
- "id": "input/text"
111
- }
112
- },
113
- {
114
- "name": "LAMBDA_NAME",
115
- "type": "Pattern",
116
- "value": "serverless-ircd-handler",
117
- "input": {
118
- "id": "input/text"
119
- }
120
- },
121
- {
122
- "name": "LOG_GROUP_ARN",
123
- "type": "Pattern",
124
- "value": "",
125
- "input": {
126
- "id": "input/text"
127
- }
128
- },
129
- {
130
- "name": "AWS_REGION",
131
- "type": "Pattern",
132
- "value": "us-east-1",
133
- "input": {
134
- "id": "input/text"
135
- }
136
- }
137
- ]
138
- }
139
- }