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/PLAN.md
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
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.
|