serverless-ircd 0.1.0 → 0.3.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 (204) hide show
  1. package/.github/workflows/ci.yml +96 -2
  2. package/.github/workflows/deploy-aws.yml +129 -0
  3. package/.github/workflows/deploy-cf.yml +0 -2
  4. package/.gitmodules +3 -0
  5. package/CHANGELOG.md +436 -0
  6. package/README.md +127 -84
  7. package/apps/aws-stack/README.md +73 -0
  8. package/apps/aws-stack/bin/aws.ts +49 -0
  9. package/apps/aws-stack/cdk.json +10 -0
  10. package/apps/aws-stack/package.json +41 -0
  11. package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
  12. package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
  13. package/apps/aws-stack/scripts/smoke.mjs +142 -0
  14. package/apps/aws-stack/src/aws-stack.ts +263 -0
  15. package/apps/aws-stack/src/tables.ts +10 -0
  16. package/apps/aws-stack/tests/localstack.test.ts +46 -0
  17. package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
  18. package/apps/aws-stack/tests/stack.test.ts +464 -0
  19. package/apps/aws-stack/tsconfig.build.json +11 -0
  20. package/apps/aws-stack/tsconfig.test.json +8 -0
  21. package/apps/aws-stack/vitest.config.ts +20 -0
  22. package/apps/cf-worker/package.json +1 -1
  23. package/apps/cf-worker/scripts/smoke.mjs +0 -0
  24. package/apps/cf-worker/src/worker.ts +8 -2
  25. package/apps/cf-worker/tests/smoke.test.ts +1 -0
  26. package/apps/cf-worker/wrangler.test.toml +5 -1
  27. package/apps/cf-worker/wrangler.toml +66 -17
  28. package/apps/local-cli/package.json +1 -1
  29. package/apps/local-cli/src/config-loader.ts +57 -0
  30. package/apps/local-cli/src/main.ts +1 -1
  31. package/apps/local-cli/src/server.ts +267 -32
  32. package/apps/local-cli/tests/config-loader.test.ts +107 -0
  33. package/apps/local-cli/tests/e2e.test.ts +126 -0
  34. package/apps/local-cli/tests/security-e2e.test.ts +239 -0
  35. package/biome.json +3 -1
  36. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  37. package/docs/ADR-002-location-of-authority.md +82 -0
  38. package/docs/ADR-003-durable-object-sharding.md +93 -0
  39. package/docs/ADR-004-dynamodb-schema.md +96 -0
  40. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  41. package/docs/ADR-006-sasl-mechanism-scope.md +86 -0
  42. package/docs/ADR-007-deterministic-ports.md +82 -0
  43. package/docs/ADR-008-monorepo-tooling.md +60 -0
  44. package/docs/AWS-Adapter-Architecture.md +496 -0
  45. package/docs/AWS-Deployment.md +1186 -0
  46. package/docs/Cloudflare-Deployment-Guide.md +660 -0
  47. package/docs/Home.md +11 -0
  48. package/docs/Observability.md +87 -0
  49. package/docs/PlanIRCv3Websocket.md +489 -0
  50. package/docs/PlanWebClient.md +451 -0
  51. package/docs/Release-Process.md +443 -0
  52. package/package.json +19 -14
  53. package/packages/aws-adapter/README.md +35 -0
  54. package/packages/aws-adapter/package.json +52 -0
  55. package/packages/aws-adapter/src/account-store.ts +121 -0
  56. package/packages/aws-adapter/src/aws-runtime.ts +871 -0
  57. package/packages/aws-adapter/src/cdk-table-defs.ts +73 -0
  58. package/packages/aws-adapter/src/config-loader.ts +126 -0
  59. package/packages/aws-adapter/src/dynamo-account-store.ts +156 -0
  60. package/packages/aws-adapter/src/dynamo.ts +61 -0
  61. package/packages/aws-adapter/src/handlers/connect.ts +44 -0
  62. package/packages/aws-adapter/src/handlers/default.ts +330 -0
  63. package/packages/aws-adapter/src/handlers/disconnect.ts +48 -0
  64. package/packages/aws-adapter/src/handlers/index.ts +293 -0
  65. package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
  66. package/packages/aws-adapter/src/handlers/state.ts +13 -0
  67. package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
  68. package/packages/aws-adapter/src/index.ts +74 -0
  69. package/packages/aws-adapter/src/message-store.ts +34 -0
  70. package/packages/aws-adapter/src/serialize.ts +283 -0
  71. package/packages/aws-adapter/src/tables.ts +53 -0
  72. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +171 -0
  73. package/packages/aws-adapter/tests/account-store.test.ts +280 -0
  74. package/packages/aws-adapter/tests/aws-harness.ts +408 -0
  75. package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
  76. package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
  77. package/packages/aws-adapter/tests/config-loader.test.ts +213 -0
  78. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -0
  79. package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
  80. package/packages/aws-adapter/tests/global-setup.ts +301 -0
  81. package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
  82. package/packages/aws-adapter/tests/handlers.test.ts +473 -0
  83. package/packages/aws-adapter/tests/message-store.test.ts +55 -0
  84. package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
  85. package/packages/aws-adapter/tests/serialize.test.ts +249 -0
  86. package/packages/aws-adapter/tests/smoke.test.ts +48 -0
  87. package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
  88. package/packages/aws-adapter/tests/tables.test.ts +74 -0
  89. package/packages/aws-adapter/tests/transactions.test.ts +446 -0
  90. package/packages/aws-adapter/tsconfig.build.json +16 -0
  91. package/packages/aws-adapter/tsconfig.test.json +14 -0
  92. package/packages/aws-adapter/vitest.config.ts +23 -0
  93. package/packages/cf-adapter/package.json +2 -1
  94. package/packages/cf-adapter/src/cf-runtime.ts +42 -22
  95. package/packages/cf-adapter/src/channel-do.ts +40 -1
  96. package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
  97. package/packages/cf-adapter/src/config-loader.ts +135 -0
  98. package/packages/cf-adapter/src/connection-do.ts +119 -0
  99. package/packages/cf-adapter/src/env.ts +27 -1
  100. package/packages/cf-adapter/src/index.ts +2 -1
  101. package/packages/cf-adapter/tests/cf-harness.ts +2 -2
  102. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
  103. package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
  104. package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
  105. package/packages/cf-adapter/tests/connection-do.test.ts +82 -0
  106. package/packages/cf-adapter/tests/worker/main.ts +2 -1
  107. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
  108. package/packages/cf-adapter/wrangler.test.toml +10 -1
  109. package/packages/in-memory-runtime/package.json +1 -1
  110. package/packages/in-memory-runtime/src/in-memory-runtime.ts +107 -16
  111. package/packages/in-memory-runtime/src/index.ts +1 -1
  112. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +134 -0
  113. package/packages/irc-core/package.json +13 -2
  114. package/packages/irc-core/src/admission.ts +216 -0
  115. package/packages/irc-core/src/caps/capabilities.ts +1 -0
  116. package/packages/irc-core/src/case-fold.ts +64 -0
  117. package/packages/irc-core/src/cloak.ts +81 -0
  118. package/packages/irc-core/src/commands/cap.ts +1 -2
  119. package/packages/irc-core/src/commands/chathistory.ts +305 -0
  120. package/packages/irc-core/src/commands/index.ts +9 -0
  121. package/packages/irc-core/src/commands/invite.ts +6 -9
  122. package/packages/irc-core/src/commands/isupport.ts +1 -1
  123. package/packages/irc-core/src/commands/join.ts +63 -10
  124. package/packages/irc-core/src/commands/kick.ts +4 -11
  125. package/packages/irc-core/src/commands/list.ts +5 -4
  126. package/packages/irc-core/src/commands/mode.ts +5 -9
  127. package/packages/irc-core/src/commands/motd-lines.ts +127 -0
  128. package/packages/irc-core/src/commands/motd.ts +6 -110
  129. package/packages/irc-core/src/commands/oper.ts +151 -0
  130. package/packages/irc-core/src/commands/privmsg.ts +24 -0
  131. package/packages/irc-core/src/commands/registration.ts +79 -21
  132. package/packages/irc-core/src/commands/sasl.ts +251 -0
  133. package/packages/irc-core/src/commands/tagmsg.ts +205 -0
  134. package/packages/irc-core/src/config.ts +270 -0
  135. package/packages/irc-core/src/flood-control.ts +175 -0
  136. package/packages/irc-core/src/index.ts +7 -0
  137. package/packages/irc-core/src/ports.ts +719 -0
  138. package/packages/irc-core/src/protocol/base64.ts +16 -0
  139. package/packages/irc-core/src/protocol/index.ts +2 -1
  140. package/packages/irc-core/src/protocol/numerics.ts +11 -0
  141. package/packages/irc-core/src/protocol/parser.ts +27 -2
  142. package/packages/irc-core/src/state/connection.ts +41 -0
  143. package/packages/irc-core/src/types.ts +88 -2
  144. package/packages/irc-core/stryker.commands.conf.json +41 -0
  145. package/packages/irc-core/stryker.protocol.conf.json +26 -0
  146. package/packages/irc-core/tests/account-store.test.ts +88 -0
  147. package/packages/irc-core/tests/admission.test.ts +229 -0
  148. package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
  149. package/packages/irc-core/tests/case-fold.test.ts +80 -0
  150. package/packages/irc-core/tests/cloak.test.ts +78 -0
  151. package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
  152. package/packages/irc-core/tests/commands/join.test.ts +246 -2
  153. package/packages/irc-core/tests/commands/kick.test.ts +15 -0
  154. package/packages/irc-core/tests/commands/oper.test.ts +257 -0
  155. package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
  156. package/packages/irc-core/tests/commands/registration.test.ts +380 -20
  157. package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
  158. package/packages/irc-core/tests/commands/tagmsg.test.ts +688 -0
  159. package/packages/irc-core/tests/commands/who.test.ts +22 -4
  160. package/packages/irc-core/tests/commands/whois.test.ts +8 -1
  161. package/packages/irc-core/tests/config.test.ts +721 -0
  162. package/packages/irc-core/tests/flood-control.test.ts +394 -0
  163. package/packages/irc-core/tests/message-store.test.ts +530 -0
  164. package/packages/irc-core/tests/parser.test.ts +44 -1
  165. package/packages/irc-core/tests/ports.test.ts +263 -0
  166. package/packages/irc-server/package.json +1 -1
  167. package/packages/irc-server/src/actor.ts +186 -44
  168. package/packages/irc-server/src/dispatch.ts +89 -5
  169. package/packages/irc-server/src/index.ts +2 -0
  170. package/packages/irc-server/src/routing.ts +160 -0
  171. package/packages/irc-server/tests/actor.test.ts +690 -15
  172. package/packages/irc-server/tests/dispatch.test.ts +84 -0
  173. package/packages/irc-server/tests/routing.test.ts +204 -0
  174. package/packages/irc-test-support/README.md +32 -0
  175. package/packages/irc-test-support/package.json +37 -0
  176. package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +6 -5
  177. package/packages/irc-test-support/src/index.ts +24 -0
  178. package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
  179. package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
  180. package/packages/irc-test-support/tests/smoke.test.ts +11 -0
  181. package/packages/irc-test-support/tsconfig.build.json +16 -0
  182. package/packages/irc-test-support/tsconfig.test.json +14 -0
  183. package/packages/irc-test-support/vitest.config.ts +20 -0
  184. package/pnpm-workspace.yaml +1 -0
  185. package/tools/ci-hardening/package.json +30 -0
  186. package/tools/ci-hardening/src/index.ts +6 -0
  187. package/tools/ci-hardening/src/validate.ts +103 -0
  188. package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
  189. package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
  190. package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
  191. package/tools/ci-hardening/tests/validate.test.ts +177 -0
  192. package/tools/ci-hardening/tsconfig.build.json +12 -0
  193. package/tools/ci-hardening/tsconfig.test.json +10 -0
  194. package/tools/ci-hardening/vitest.config.ts +21 -0
  195. package/tools/seed-aws-accounts.ts +80 -0
  196. package/tools/tcp-ws-forwarder/package.json +1 -1
  197. package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
  198. package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
  199. package/tools/tcp-ws-forwarder/src/main.ts +33 -14
  200. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
  201. package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
  202. package/packages/cf-adapter/tests/integration/index.ts +0 -17
  203. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
  204. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
package/README.md CHANGED
@@ -6,14 +6,20 @@ platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
6
6
 
7
7
  One TypeScript codebase. Two serverless substrates. No S2S linking in v1.
8
8
 
9
- > **Status:** **v0.1.0 (preview).** The pure protocol core, the
9
+ > **Status:** **v0.3.0 (preview).** The pure protocol core, the
10
10
  > `IrcRuntime` port + in-memory runtime, a runnable local CLI server,
11
- > and the Cloudflare Workers adapter (deployed to staging) are all
12
- > functional. SASL, flood control, the AWS adapter, server-password
13
- > enforcement, and the CI coverage gate are still ahead. See
14
- > `CHANGELOG.md` for the v0.1.0 manifest, `docs/release.md` for the
15
- > release process, and `progress.md` / `tickets.md` for per-ticket
16
- > status.
11
+ > the **Cloudflare Workers** adapter, and the **AWS** (API Gateway
12
+ > WebSocket + Lambda + DynamoDB + CDK) adapter are all functional and
13
+ > deployed to staging. v0.2.0 delivered SASL (`PLAIN`), token-bucket
14
+ > flood control, server-password enforcement, hostmask cloaking,
15
+ > connection admission, and IRCv3 chat history; v0.3.0 adds **OPER**
16
+ > credential auth, the IRCv3 **TAGMSG** command, **RFC 1459
17
+ > case-mapping**, a real **MOTD** in the registration welcome, a
18
+ > **ChannelRegistryDO** (4th Cloudflare Durable Object) plus a
19
+ > Cloudflare config loader, and **SASL account persistence** in
20
+ > DynamoDB (scrypt hashes) with a `seed-aws-accounts` provisioning
21
+ > tool. Remaining 0.x work: SASL `EXTERNAL` (mTLS) and production TLS
22
+ > transport. See `CHANGELOG.md` for the per-release manifests.
17
23
 
18
24
  ---
19
25
 
@@ -52,16 +58,17 @@ Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
52
58
  └─────────────────────────────────────────────────────────────────┘
53
59
  ▲ ▲
54
60
  ┌────────────────────────────┐ ┌──────────────────────────────┐
55
- adapters/cf │ │ adapters/aws
61
+ packages/cf-adapter │ │ packages/aws-adapter
56
62
  │ ConnectionDO │ │ $connect/$disconnect/ │
57
63
  │ ChannelDO │ │ $default Lambda handlers │
58
- RegistryDO │ │ DynamoDB tables │
59
- CfRuntime │ │ AwsRuntime │
64
+ ChannelRegistryDO │ │ DynamoDB tables │
65
+ RegistryDO │ │ AwsRuntime │
66
+ │ CfRuntime │ │ │
60
67
  └────────────────────────────┘ └──────────────────────────────┘
61
68
  ```
62
69
 
63
- A planned `packages/in-memory-runtime` provides a reference implementation of
64
- `IrcRuntime` used by integration tests and a local CLI server.
70
+ `packages/in-memory-runtime` is the reference implementation of
71
+ `IrcRuntime`, used by integration tests and the local CLI server.
65
72
 
66
73
  ### The key idea: pure reducers + location-of-authority
67
74
 
@@ -98,34 +105,37 @@ ServerlessIRCd/
98
105
  ├── packages/
99
106
  │ ├── irc-core/ pure protocol + reducers (the brain)
100
107
  │ ├── irc-server/ orchestration, IrcRuntime port, dispatch
101
- │ ├── in-memory-runtime/ reference runtime + local CLI server (planned)
102
- │ ├── cf-adapter/ Durable Objects + CfRuntime (planned)
103
- └── aws-adapter/ Lambda + DynamoDB + AwsRuntime (planned)
108
+ │ ├── in-memory-runtime/ reference runtime (used by tests + local CLI)
109
+ │ ├── irc-test-support/ parametrized IRC scenario suite + harness seam
110
+ ├── cf-adapter/ Durable Objects + CfRuntime
111
+ │ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime
104
112
  ├── apps/
105
- │ ├── cf-worker/ worker entry, DO migrations, bindings (planned)
106
- │ ├── aws-stack/ CDK app entry (planned)
107
- │ └── local-cli/ runnable WS server using in-memory-runtime
113
+ │ ├── cf-worker/ worker entry, DO migrations, bindings
114
+ │ ├── aws-stack/ CDK stack (APIGW WS + Lambda + DynamoDB)
115
+ │ └── local-cli/ runnable WS + TCP server using in-memory-runtime
108
116
  ├── tools/
109
- └── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
110
- ├── docs/
111
- │ └── architecture-aws.md
117
+ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
118
+ ├── ci-hardening/ coverage-gate + mutation-config validators
119
+ │ └── seed-aws-accounts.ts scrypt-hash SASL PLAIN accounts into DynamoDB
112
120
  ├── pnpm-workspace.yaml turbo.json tsconfig.base.json
113
- └── PLAN.md progress.md tickets.md
121
+ └── README.md CHANGELOG.md
114
122
  ```
115
123
 
116
124
  ---
117
125
 
118
126
  ## Toolchain
119
127
 
120
- | Concern | Choice |
121
- |----------------|---------------------------------------------------|
122
- | Runtime | Node ≥ 20, ES2022+ |
123
- | Package mgr | pnpm workspaces |
124
- | Build cache | turbo |
125
- | Language | TypeScript (strict, `exactOptionalPropertyTypes`) |
126
- | Test runner | vitest + `@vitest/coverage-v8` |
127
- | Property tests | fast-check |
128
- | Lint / format | Biome |
128
+ | Concern | Choice |
129
+ |------------------|---------------------------------------------------|
130
+ | Runtime | Node ≥ 20, ES2022+ |
131
+ | Package mgr | pnpm workspaces |
132
+ | Build cache | turbo |
133
+ | Language | TypeScript (strict, `exactOptionalPropertyTypes`) |
134
+ | Test runner | vitest + `@vitest/coverage-v8` |
135
+ | Property tests | fast-check |
136
+ | Mutation tests | Stryker (≥80% score gate on irc-core) |
137
+ | AWS IaC | AWS CDK v2 |
138
+ | Lint / format | Biome |
129
139
 
130
140
  ---
131
141
 
@@ -140,26 +150,37 @@ pnpm build # build all packages (turbo)
140
150
  pnpm typecheck # tsc --noEmit across the workspace
141
151
  pnpm test # run all tests once
142
152
  pnpm test:watch # vitest watch mode
143
- pnpm coverage # tests + v8 coverage report
153
+ pnpm coverage # tests + v8 coverage report (enforces thresholds)
144
154
 
145
155
  pnpm lint # biome check .
146
156
  pnpm lint:fix # biome check --write .
147
157
  pnpm format # biome format --write .
148
158
 
159
+ pnpm mutation # Stryker spot-check on irc-core (protocol + commands)
160
+
149
161
  pnpm clean # remove dist/coverage/.turbo + node_modules
150
162
  ```
151
163
 
152
164
  Coverage reports are written to `packages/*/coverage/`. CI (`.github/workflows/ci.yml`)
153
- runs lint, typecheck, and coverage on every push and pull request.
165
+ runs lint, typecheck, the coverage gate, the parametrized contract suite, and a
166
+ Stryker mutation spot-check on every push and pull request. Coverage thresholds
167
+ enforce 100% on `irc-core` / `irc-server` / `in-memory-runtime` and ≥90% on every
168
+ other package.
154
169
 
155
170
  ---
156
171
 
157
172
  ## Running the local server
158
173
 
159
- `apps/local-cli` is a runnable WebSocket IRC server built on the in-memory
160
- runtime. It's the manual-test harness and the e2e fixture target — the same
161
- `irc-core` reducers and `ConnectionActor` the cloud adapters will use, just
162
- wired to a plain `ws.WebSocketServer` on localhost.
174
+ `apps/local-cli` is a runnable IRC server built on the in-memory runtime.
175
+ It's the manual-test harness and the e2e fixture target — the same
176
+ `irc-core` reducers and `ConnectionActor` the cloud adapters use, just
177
+ wired to plain localhost listeners. By default it binds **two** listeners
178
+ that share one runtime, so a TCP client and a WS client can see each other
179
+ (cross-transport channel broadcast):
180
+
181
+ - a **WebSocket** listener (the serverless transport) on `--port`, and
182
+ - an **RFC-style TCP** listener on `--tcp-port` (default `<port> + 1`) so
183
+ real IRC clients (WeeChat, HexChat, irssi, …) can dial in directly.
163
184
 
164
185
  First build the workspace (the CLI runs from compiled `dist/`):
165
186
 
@@ -178,37 +199,50 @@ pnpm --filter local-cli start --port 6667 --host 127.0.0.1
178
199
  On startup it logs a JSON line like:
179
200
 
180
201
  ```json
181
- {"ts":"...","level":"info","msg":"local-cli listening","url":"ws://127.0.0.1:6667/","port":6667,"hostname":"127.0.0.1"}
202
+ {"ts":"...","level":"info","msg":"local-cli listening","wsUrl":"ws://127.0.0.1:6667/","port":6667,"tcpPort":6668,"hostname":"127.0.0.1"}
182
203
  ```
183
204
 
184
205
  ### CLI flags
185
206
 
186
- | Flag | Default | Description |
187
- |--------------|-------------|----------------------------------------------|
188
- | `--port <n>` | `6667` | TCP port to bind. Use `0` for an ephemeral. |
189
- | `--host <h>` | `127.0.0.1` | Hostname / interface to bind. |
190
- | `-h, --help` | | Show help and exit. |
207
+ | Flag | Default | Description |
208
+ |--------------------|---------------|--------------------------------------------------------|
209
+ | `--port <n>` | `6667` | WebSocket port to bind. Use `0` for an ephemeral. |
210
+ | `--host <h>` | `127.0.0.1` | Hostname / interface to bind. |
211
+ | `--tcp-port <n>` | `<port> + 1` | TCP port for RFC-style IRC clients. |
212
+ | `--no-tcp` | | Disable the TCP listener (WebSocket only). |
213
+ | `--motd-file <p>` | built-in | Read MOTD lines from this file (one per line). |
214
+ | `-h, --help` | | Show help and exit. |
191
215
 
192
216
  Pass `--host 0.0.0.0` to expose the server on all interfaces. `SIGINT` /
193
217
  `SIGTERM` perform a graceful shutdown (closes active sockets, then exits).
194
218
 
195
219
  ### Connecting
196
220
 
197
- The server speaks WebSocket text frames (one IRC message per frame). Point any
198
- WebSocket-capable IRC client (WeeChat with the `irc` protocol's WS relay, or a
199
- small script) at `ws://127.0.0.1:6667/`. Server name and MOTD are fixed
200
- defaults (`irc.example.com`) until the config-loader ticket lands.
221
+ The WebSocket listener speaks WebSocket text frames (one IRC message per
222
+ frame) — point any WebSocket-capable IRC client or script at
223
+ `ws://127.0.0.1:6667/`. The TCP listener speaks plain RFC IRC over the
224
+ wire, so stock clients connect directly, e.g. in WeeChat:
225
+
226
+ ```
227
+ /connect 127.0.0.1/6668
228
+ ```
229
+
230
+ No `tcp-ws-forwarder` is needed for local development — both transports
231
+ are built in. The MOTD is the built-in default unless `--motd-file` is
232
+ given.
201
233
 
202
234
  ---
203
235
 
204
- ## Connecting a TCP IRC client (TCP→WS forwarder)
236
+ ## Connecting a TCP IRC client to a deployed stack (TCP→WS forwarder)
205
237
 
206
- v1 is WebSocket-only, so a stock TCP IRC client (WeeChat, HexChat, irssi, …)
207
- cannot dial a deployed worker or the local WS server directly.
208
- `tools/tcp-ws-forwarder` is a local bridge: it listens on a TCP port and, for
209
- each connection, opens one WebSocket to a `ws://` / `wss://` target and pumps
210
- IRC lines both ways reassembling the TCP byte stream into one message per WS
211
- frame outbound, and normalizing inbound frames back to canonical CRLF.
238
+ The local CLI ships a built-in TCP listener, so no bridge is needed for local
239
+ development. Deployed stacks (Cloudflare Workers and AWS API Gateway) are
240
+ WebSocket-only, so a stock TCP IRC client (WeeChat, HexChat, irssi, …) cannot
241
+ dial them directly. `tools/tcp-ws-forwarder` is a local bridge for that case:
242
+ it listens on a TCP port and, for each connection, opens one WebSocket to a
243
+ `ws://` / `wss://` target and pumps IRC lines both ways reassembling the TCP
244
+ byte stream into one message per WS frame outbound, and normalizing inbound
245
+ frames back to canonical CRLF.
212
246
 
213
247
  Build the workspace, then start the forwarder (from the repo root):
214
248
 
@@ -255,14 +289,15 @@ This project follows strict TDD (Red → Green → Refactor) — every reducer i
255
289
  landed test-first. See the project-level `CLAUDE.md` / `AGENTS.md` for the
256
290
  full rules.
257
291
 
258
- | Layer | Tooling | What it asserts |
259
- |------------------|-----------------------------------------|---------------------------------------|
260
- | Parser/serializer| vitest + fast-check | Grammar correctness, round-trip |
261
- | Reducers (core) | vitest pure unit tests | `(state,msg) → {state,effects}` exact |
262
- | Runtime contract | vitest parametrized over `IrcRuntime` | Same scenarios pass in-memory+CF+AWS |
263
- | CF adapter | `vitest-pool-workers` (real workerd) | DO behavior, alarms, stub fanout |
264
- | AWS adapter | vitest + dynamodb-local / localstack | DynamoDB schema, transactions, fanout |
265
- | E2E | real WS clients in `tools/irc-client` | RFC-shaped flows vs deployed stack |
292
+ | Layer | Tooling | What it asserts |
293
+ |--------------------|---------------------------------------------|---------------------------------------|
294
+ | Parser/serializer | vitest + fast-check | Grammar correctness, round-trip |
295
+ | Reducers (core) | vitest pure unit tests | `(state,msg) → {state,effects}` exact |
296
+ | Mutation (core) | Stryker spot-check (≥80% score gate) | Reducer test quality, surviving mutants |
297
+ | Runtime contract | vitest parametrized via `irc-test-support` | Same scenarios pass in-memory+CF+AWS |
298
+ | CF adapter | `vitest-pool-workers` (real workerd) | DO behavior, alarms, stub fanout |
299
+ | AWS adapter | vitest + testcontainers (DynamoDB Local) | DynamoDB schema, transactions, fanout |
300
+ | E2E | `scripts/smoke.mjs` against deployed stack | CONNECT→REGISTER→JOIN→PRIVMSG→QUIT |
266
301
 
267
302
  **Determinism:** reducers never touch real timers or randomness. A `Clock`
268
303
  port and an `IdFactory` port are injected so tests are fully deterministic.
@@ -274,20 +309,22 @@ port and an `IdFactory` port are injected so tests are fully deterministic.
274
309
  A v1 ships when `irc-core` is at 100% coverage, the parametrized contract
275
310
  suite passes against every runtime, both adapters are deployed to staging,
276
311
  and ≥3 reference clients (WeeChat, HexChat, IRCCloud, TheLounge) connect
277
- cleanly. Full per-ticket status lives in `progress.md`. Per-release
278
- manifests live in `CHANGELOG.md`; the release process is documented in
279
- `docs/release.md`.
312
+ cleanly. Per-release manifests live in `CHANGELOG.md`.
280
313
 
281
314
  - **Phase 0** — Foundation (monorepo, turbo, vitest, biome, CI). ✅
282
- - **Phase 1** — Pure protocol engine: reducers, IRCv3 caps, isupport.
283
- Most of this landed in **v0.1.0**; SASL and flood control are still
284
- pending.
315
+ - **Phase 1** — Pure protocol engine: reducers, IRCv3 caps, isupport,
316
+ SASL (`PLAIN`), token-bucket flood control, chat history. (most in
317
+ **v0.1.0**; SASL/flood/chathistory landed in **v0.2.0**).
285
318
  - **Phase 2** — `IrcRuntime` port, in-memory runtime, `ConnectionActor`,
286
319
  local CLI, parametrized contract suite. ✅ landed in **v0.1.0**.
287
320
  - **Phase 3** — Cloudflare adapter (ConnectionDO / ChannelDO / RegistryDO).
288
- ✅ landed in **v0.1.0** (staging only; prod deploy is manual).
321
+ ✅ landed in **v0.1.0** (staging auto-deploy via CI; prod deploy is manual).
289
322
  - **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
323
+ ✅ landed in **v0.2.0** (staging auto-deploy via CI; prod deploy is manual).
290
324
  - **Phase 5** — Observability, security hardening, config, CI gates.
325
+ ✅ landed (logger port, server-password, cloaking, admission limits,
326
+ coverage + mutation gates, OPER credential auth, and the Cloudflare
327
+ config loader all shipped across v0.2.0–v0.3.0).
291
328
  - **Phase 6** — Load testing, client compatibility sweep, ADRs.
292
329
 
293
330
  ---
@@ -296,28 +333,34 @@ manifests live in `CHANGELOG.md`; the release process is documented in
296
333
 
297
334
  **Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
298
335
  `PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
299
- channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD`.
336
+ channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD`,
337
+ `AWAY`, `OPER` (credential auth → `o` user mode).
300
338
 
301
339
  **Channel modes:** `o v b i k l t n m s p`.
302
340
  **User modes:** `i`, `o` (local only), `w`, `s`.
303
341
 
304
- **IRCv3 extensions (negotiated via `CAP`):** `message-tags`, `server-time`,
305
- `account-tag`, `echo-message`, `batch`, `sasl` (`PLAIN`, `EXTERNAL` reserved),
306
- `multi-prefix`, `away-notify`, `chghost`, `invite-notify`, `extended-join`.
342
+ **IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
343
+ `TAGMSG` command), `server-time`, `account-tag`, `echo-message`, `batch`,
344
+ `sasl` (`PLAIN` implemented, `EXTERNAL` reserved pending mTLS),
345
+ `multi-prefix`, `away-notify`, `chghost`, `invite-notify`, `extended-join`,
346
+ `draft/chathistory`, `safelist`. Case-insensitive nick/channel comparison
347
+ uses **RFC 1459** case-mapping (advertised via `005 CASEMAPPING=rfc1459`).
307
348
 
308
- **Transport:** WebSocket text frames only (one IRC message per frame, with
309
- tolerance for `\r\n`-joined frames). No plaintext TCP in v1 keeps the
310
- serverless story clean. Stock TCP IRC clients reach the server through the
311
- local [`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-tcpws-forwarder).
349
+ **Transport:** deployed stacks (Cloudflare Workers, AWS API Gateway) speak
350
+ WebSocket text frames only (one IRC message per frame, with tolerance for
351
+ `\r\n`-joined frames). The local CLI additionally binds a plain RFC TCP
352
+ listener for direct IRC-client access. Stock TCP IRC clients reach a deployed
353
+ WebSocket endpoint through the local
354
+ [`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder).
312
355
 
313
356
  ---
314
357
 
315
358
  ## Further reading
316
359
 
317
- - `PLAN.md` — full project plan, platform mappings, risks, decisions.
318
- - `CHANGELOG.md` per-release manifests (Keep a Changelog format).
319
- - `docs/release.md` release process runbook (versioning, tagging,
320
- rollback).
321
- - `progress.md` / `tickets.md` — what's done, what's next.
322
- - `docs/deployment-cf.md` Cloudflare deployment guide.
323
- - `docs/architecture-aws.md` AWS adapter design notes.
360
+ - `CHANGELOG.md` — per-release manifests (Keep a Changelog format), including
361
+ the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
362
+ gates) and v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
363
+ ChannelRegistryDO, SASL account persistence).
364
+ - `README.md` in each `packages/*` and `apps/*`per-package notes (e.g.
365
+ `packages/aws-adapter/README.md` for the DynamoDB-Local test setup,
366
+ `apps/aws-stack/README.md` for CDK commands and localstack validation).
@@ -0,0 +1,73 @@
1
+ # `@serverless-ircd/aws-stack`
2
+
3
+ AWS CDK v2 stack that provisions the AWS substrate for ServerlessIRCd
4
+ (PLAN §6.2): an API Gateway v2 WebSocket API, a single Node 20 Lambda wired
5
+ to the `$connect` / `$disconnect` / `$default` routes, five DynamoDB tables
6
+ (`Connections`, `ChannelMeta`, `ChannelMembers`, `Nicks`, `Accounts`), and the
7
+ least-privilege IAM glue to let the Lambda read/write the tables and post back
8
+ to connected clients via the management API.
9
+
10
+ The Lambda handler is currently a stub (`{ statusCode: 200 }`); the real
11
+ `AwsRuntime` dispatch lands in TICKET-040.
12
+
13
+ ## Prerequisites
14
+
15
+ - Node.js 20+
16
+ - pnpm (workspace root installs this package)
17
+ - An AWS account with credentials configured
18
+ (`aws configure`, `AWS_PROFILE`, or `SSO`)
19
+
20
+ ## Commands
21
+
22
+ ```sh
23
+ pnpm cdk:synth # synthesize the CloudFormation template (cdk.out/)
24
+ pnpm deploy:staging # cdk deploy --all
25
+ pnpm deploy:prod # cdk deploy --all --require-approval never
26
+ pnpm test # synth-time assertions (no AWS, no Docker)
27
+ pnpm typecheck
28
+ ```
29
+
30
+ Repo-level shortcuts (from the monorepo root):
31
+
32
+ ```sh
33
+ pnpm deploy:aws:staging
34
+ pnpm deploy:aws:prod
35
+ pnpm smoke:aws:staging # deferred to TICKET-043
36
+ ```
37
+
38
+ ## Optional localstack validation
39
+
40
+ The default `pnpm test` runs only the in-process synth assertions
41
+ (`tests/stack.test.ts`). A deploy-time check against localstack lives in
42
+ `tests/localstack.test.ts` and is **skipped unless `LOCALSTACK=1`** is set,
43
+ so CI never depends on a Docker daemon.
44
+
45
+ To run it locally:
46
+
47
+ ```sh
48
+ # 1. Start localstack (requires Docker)
49
+ docker run --rm -d -p 4566:4566 localstack/localstack
50
+
51
+ # 2. Install the localstack-aware CDK CLI globally
52
+ npm install -g aws-cdk-local
53
+
54
+ # 3. Point the AWS SDK at localstack (cdklocal reads these)
55
+ export AWS_ACCESS_KEY_ID=test
56
+ export AWS_SECRET_ACCESS_KEY=test
57
+ export AWS_DEFAULT_REGION=us-east-1
58
+
59
+ # 4. Run the gated test
60
+ LOCALSTACK=1 pnpm test
61
+ ```
62
+
63
+ ## Stack outputs
64
+
65
+ | Output | Description |
66
+ |----------------|--------------------------------------------------------------------|
67
+ | `ConnectUrl` | `wss://` endpoint IRC clients dial. |
68
+ | `ManagementUrl`| `https://` endpoint for `ApiGatewayManagementApi.postToConnection`.|
69
+
70
+ ## Handoff
71
+
72
+ Lambda runtime logic, DynamoDB access patterns, and the `AwsRuntime`
73
+ implementation are TICKET-040. The scheduler/sweeper is TICKET-041/042.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * CDK app entry point for `apps/aws-stack`.
3
+ *
4
+ * Run via `tsx bin/aws.ts` (see `cdk.json`). Environment resolves to the
5
+ * CDK CLI's default account/region when those env vars are set; otherwise
6
+ * the stack is environment-agnostic (suitable for `cdk synth` and localstack).
7
+ *
8
+ * Per-environment isolation is driven by the `environmentName` CDK context
9
+ * variable (`-c environmentName=staging`). It defaults to `staging` so the
10
+ * existing deploy pipeline stays green without changes, and it is folded
11
+ * into the CloudFormation stack id (`IrcAwsStack-<environmentName>`) so
12
+ * parallel envs coexist in one account+region.
13
+ */
14
+
15
+ import { App } from 'aws-cdk-lib';
16
+ import type { IrcStackProps } from '../src/aws-stack.js';
17
+ import { IrcAwsStack } from '../src/aws-stack.js';
18
+
19
+ const app = new App();
20
+
21
+ const account = process.env.CDK_DEFAULT_ACCOUNT;
22
+ const region = process.env.CDK_DEFAULT_REGION;
23
+ const environmentName =
24
+ (app.node.tryGetContext('environmentName') as string | undefined) ?? 'staging';
25
+
26
+ // Server identity is read from CDK context so deploys can override it
27
+ // without editing source (`-c serverName=…`, `-c networkName=…`,
28
+ // `-c motd=…`). `motd` may be passed as a `\n`-embedded string from the
29
+ // CLI; split it back into lines so the stack re-joins with `\n` and the
30
+ // Lambda config loader round-trips it correctly. Defaults (omitted here)
31
+ // live on the stack itself.
32
+ const serverName = app.node.tryGetContext('serverName') as string | undefined;
33
+ const networkName = app.node.tryGetContext('networkName') as string | undefined;
34
+ const rawMotd = app.node.tryGetContext('motd');
35
+ const motdLines = Array.isArray(rawMotd)
36
+ ? (rawMotd as string[])
37
+ : typeof rawMotd === 'string'
38
+ ? rawMotd.split('\n')
39
+ : undefined;
40
+
41
+ const stackProps: IrcStackProps = {
42
+ environmentName,
43
+ ...(serverName !== undefined ? { serverName } : {}),
44
+ ...(networkName !== undefined ? { networkName } : {}),
45
+ ...(motdLines !== undefined ? { motdLines } : {}),
46
+ ...(account && region ? { env: { account, region } } : {}),
47
+ };
48
+
49
+ new IrcAwsStack(app, `IrcAwsStack-${environmentName}`, stackProps);
@@ -0,0 +1,10 @@
1
+ {
2
+ "app": "tsx bin/aws.ts",
3
+ "context": {
4
+ "environmentName": "staging"
5
+ },
6
+ "watch": {
7
+ "include": ["src/**", "bin/**"],
8
+ "exclude": ["tests/**", "dist/**", "node_modules/**"]
9
+ }
10
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@serverless-ircd/aws-stack",
3
+ "version": "0.2.0",
4
+ "private": true,
5
+ "description": "AWS CDK v2 stack: API Gateway v2 WebSocket API, Lambda, DynamoDB tables, least-privilege IAM",
6
+ "license": "BSD-3-Clause",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.build.json",
10
+ "typecheck": "tsc -p tsconfig.test.json --noEmit",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "coverage": "vitest run --coverage",
14
+ "clean": "rimraf dist coverage .tsbuildinfo .turbo cdk.out",
15
+ "cdk": "cdk",
16
+ "cdk:synth": "cdk synth",
17
+ "deploy:staging": "cdk deploy --all --require-approval never",
18
+ "deploy:prod": "cdk deploy --all --require-approval never",
19
+ "smoke:staging": "node scripts/smoke.mjs"
20
+ },
21
+ "devDependencies": {
22
+ "aws-cdk": "^2.160.0",
23
+ "aws-cdk-lib": "^2.160.0",
24
+ "constructs": "^10.4.0",
25
+ "@types/node": "^26.1.1",
26
+ "@types/ws": "^8.5.13",
27
+ "esbuild": "^0.28.1",
28
+ "rimraf": "^6.0.0",
29
+ "tsx": "^4.19.0",
30
+ "typescript": "^5.6.0",
31
+ "vitest": "^4.1.0",
32
+ "@vitest/coverage-v8": "^4.1.0",
33
+ "ws": "^8.18.0"
34
+ },
35
+ "dependencies": {
36
+ "@serverless-ircd/aws-adapter": "workspace:*"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Type declarations for `scripts/smoke-helpers.mjs` (plain ESM consumed by the
3
+ * AWS staging smoke e2e). The runtime file ships as `.mjs` so CI can run it
4
+ * without a build step; this declaration lets the workspace typecheck resolve
5
+ * the imports from `tests/smoke-helpers.test.ts`.
6
+ */
7
+
8
+ export type ParseResult = { url: string; help: boolean };
9
+
10
+ export declare const DEFAULT_SMOKE_URL: string;
11
+
12
+ export declare function numericEquals(line: string, code: string): boolean;
13
+
14
+ export declare function parseArgs(argv: string[]): ParseResult;
15
+
16
+ export declare function waitForPredicate(
17
+ predicate: (line: string) => boolean,
18
+ label: string,
19
+ received: string[],
20
+ timeoutMs: number,
21
+ pollMs: number,
22
+ ): Promise<string>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Pure helpers backing the AWS staging smoke e2e (`scripts/smoke.mjs`).
3
+ *
4
+ * Kept dependency-free and side-effect-free (apart from the unavoidable
5
+ * `setTimeout` polling inside `waitForPredicate`) so they can be unit-tested
6
+ * in isolation. The smoke script itself owns the WebSocket I/O and process
7
+ * exit codes; these helpers own the parsing and timing logic.
8
+ */
9
+
10
+ /** Default WebSocket target when no `--url` is passed (matches the local dev convention). */
11
+ export const DEFAULT_SMOKE_URL = 'ws://localhost:8787';
12
+
13
+ /**
14
+ * Numeric command position in an IRC line (after an optional `:server`
15
+ * prefix). Recognises both `001 :Welcome` and `:irc.example.com 001 ...`.
16
+ */
17
+ export function numericEquals(line, code) {
18
+ const parts = line.split(' ');
19
+ const start = parts[0]?.startsWith(':') ? 1 : 0;
20
+ return parts[start] === code;
21
+ }
22
+
23
+ /**
24
+ * Parse the smoke script's CLI args. Returns `{ url, help }` — `url` always
25
+ * carries a sensible default, `help` is set when `-h`/`--help` was requested
26
+ * so the caller can print usage and exit 0. Throws on unknown flags or a
27
+ * dangling `--url` with no value.
28
+ *
29
+ * @param {string[]} argv
30
+ * @returns {{ url: string; help: boolean }}
31
+ */
32
+ export function parseArgs(argv) {
33
+ let url = DEFAULT_SMOKE_URL;
34
+ let help = false;
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ const next = argv[i + 1];
38
+ if (a === '--') {
39
+ // POSIX options terminator — `pnpm <script> -- --url x` forwards it.
40
+ // Nothing positional follows for this script; just consume and continue.
41
+ continue;
42
+ }
43
+ if (a === '--url') {
44
+ if (next === undefined) {
45
+ throw new Error('--url requires a value');
46
+ }
47
+ url = next;
48
+ i++;
49
+ } else if (a === '-h' || a === '--help') {
50
+ help = true;
51
+ } else {
52
+ throw new Error(`unknown argument: ${a}`);
53
+ }
54
+ }
55
+ return { url, help };
56
+ }
57
+
58
+ /**
59
+ * Resolve with the first entry in `received` that satisfies `predicate`, or
60
+ * reject with a descriptive message once `timeoutMs` elapses. Polls every
61
+ * `pollMs` so a live socket can keep appending to `received` (by reference)
62
+ * between checks.
63
+ *
64
+ * @param {(line: string) => boolean} predicate
65
+ * @param {string} label
66
+ * @param {string[]} received
67
+ * @param {number} timeoutMs
68
+ * @param {number} pollMs
69
+ * @returns {Promise<string>}
70
+ */
71
+ export function waitForPredicate(predicate, label, received, timeoutMs, pollMs) {
72
+ return new Promise((resolve, reject) => {
73
+ const deadline = Date.now() + timeoutMs;
74
+ const check = () => {
75
+ for (const line of received) {
76
+ if (predicate(line)) {
77
+ resolve(line);
78
+ return;
79
+ }
80
+ }
81
+ if (Date.now() >= deadline) {
82
+ reject(new Error(`timed out waiting for ${label}; received=${JSON.stringify(received)}`));
83
+ return;
84
+ }
85
+ setTimeout(check, pollMs);
86
+ };
87
+ check();
88
+ });
89
+ }