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
@@ -39,7 +39,7 @@ jobs:
39
39
  - name: Setup Node
40
40
  uses: actions/setup-node@v4
41
41
  with:
42
- node-version: 20
42
+ node-version: 24
43
43
  cache: pnpm
44
44
 
45
45
  - name: Install dependencies
@@ -112,7 +112,7 @@ jobs:
112
112
  - name: Setup Node
113
113
  uses: actions/setup-node@v4
114
114
  with:
115
- node-version: 20
115
+ node-version: 24
116
116
  cache: pnpm
117
117
 
118
118
  - name: Install dependencies
@@ -63,13 +63,11 @@ jobs:
63
63
 
64
64
  - name: Setup pnpm
65
65
  uses: pnpm/action-setup@v4
66
- with:
67
- version: 9
68
66
 
69
67
  - name: Setup Node
70
68
  uses: actions/setup-node@v4
71
69
  with:
72
- node-version: 20
70
+ node-version: 24
73
71
  cache: pnpm
74
72
 
75
73
  - name: Install dependencies
@@ -0,0 +1,87 @@
1
+ name: deploy-cf-tcp
2
+
3
+ # Cloudflare TCP+TLS deploy pipeline (Spectrum + Container origin).
4
+ #
5
+ # Deploys the IRC TCP container origin (apps/cf-tcp-container) to Cloudflare
6
+ # Containers and applies the Spectrum TLS config (terraform). Spectrum
7
+ # terminates TLS on :6697 and forwards plaintext TCP to the container.
8
+ #
9
+ # This pipeline runs alongside deploy-cf.yml (the WebSocket path). The two
10
+ # are independent: a deployment can enable either or both transports.
11
+ #
12
+ # Secrets (configure under repo Settings → Secrets and variables → Actions):
13
+ # CLOUDFLARE_API_TOKEN Spectrum/Containers API token.
14
+ # CLOUDFLARE_ACCOUNT_ID Account ID for the target zone.
15
+ # CF_ZONE_ID Zone ID for the Spectrum hostname.
16
+ # CF_TCP_ORIGIN_ADDRESS Hostname/IP of the container origin.
17
+
18
+ on:
19
+ push:
20
+ branches: [main]
21
+ paths:
22
+ - 'apps/cf-tcp-container/**'
23
+ - '.github/workflows/deploy-cf-tcp.yml'
24
+ workflow_dispatch:
25
+
26
+ concurrency:
27
+ group: cf-tcp-staging
28
+ cancel-in-progress: false
29
+
30
+ jobs:
31
+ deploy-staging:
32
+ name: deploy cf-tcp staging
33
+ runs-on: ubuntu-latest
34
+ timeout-minutes: 15
35
+ steps:
36
+ - name: Checkout
37
+ uses: actions/checkout@v4
38
+
39
+ - name: Setup pnpm
40
+ uses: pnpm/action-setup@v4
41
+
42
+ - name: Setup Node
43
+ uses: actions/setup-node@v4
44
+ with:
45
+ node-version: 24
46
+ cache: pnpm
47
+
48
+ - name: Install dependencies
49
+ run: pnpm install --frozen-lockfile
50
+
51
+ - name: Build workspace
52
+ run: pnpm build
53
+
54
+ - name: Typecheck
55
+ run: pnpm typecheck
56
+
57
+ - name: Test
58
+ run: pnpm --filter '@serverless-ircd/cf-tcp-container' test
59
+
60
+ - name: Deploy container to staging
61
+ working-directory: apps/cf-tcp-container
62
+ env:
63
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
64
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
65
+ run: wrangler deploy --env staging
66
+
67
+ - name: Apply Spectrum TLS config
68
+ working-directory: apps/cf-tcp-container/terraform
69
+ env:
70
+ TF_VAR_zone_id: ${{ secrets.CF_ZONE_ID }}
71
+ TF_VAR_origin_address: ${{ secrets.CF_TCP_ORIGIN_ADDRESS }}
72
+ TF_VAR_hostname: irc-staging.example.com
73
+ run: |
74
+ terraform init
75
+ terraform apply -auto-approve
76
+
77
+ - name: Smoke e2e (irc+tls)
78
+ if: ${{ vars.CF_TCP_SMOKE_HOST != '' }}
79
+ env:
80
+ SMOKE_HOST: ${{ vars.CF_TCP_SMOKE_HOST }}
81
+ SMOKE_PORT: '6697'
82
+ run: |
83
+ # Minimal smoke: connect over TLS, register, join, privmsg, quit.
84
+ # Uses openssl s_client for the TLS handshake + IRC line exchange.
85
+ echo "NICK smoke\r\nUSER smoke 0 * :Smoke\r\nQUIT :smoke\r\n" | \
86
+ timeout 10 openssl s_client -connect "${SMOKE_HOST}:${SMOKE_PORT}" -quiet 2>/dev/null | \
87
+ grep -q "001" && echo "SMOKE PASS" || (echo "SMOKE FAIL" && exit 1)
@@ -49,7 +49,7 @@ jobs:
49
49
  - name: Setup Node
50
50
  uses: actions/setup-node@v4
51
51
  with:
52
- node-version: 20
52
+ node-version: 24
53
53
  cache: pnpm
54
54
 
55
55
  - name: Install dependencies
package/.node-version ADDED
@@ -0,0 +1 @@
1
+ 24
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 24
package/CHANGELOG.md CHANGED
@@ -10,6 +10,248 @@ For the release process itself — versioning policy, pre-release checklist,
10
10
  cutting a tag, rolling back — see [`docs/release.md`](docs/release.md).
11
11
  Cross-reference `progress.md` / `tickets.md` for per-ticket detail.
12
12
 
13
+ ## [0.4.0] - 2026-07-31
14
+
15
+ The headline is **dual transport**: both adapters now speak a real
16
+ `irc+tls :6697` (TLS-over-TCP) surface for stock IRC clients in
17
+ production — not just the local CLI — alongside the WebSocket path. This
18
+ lands **SASL `EXTERNAL` via mTLS**, the **remaining standard IRC verbs**
19
+ (WHOWAS, VERSION, TIME, ADMIN, INFO, USERHOST, ISON), a **Cloudflare
20
+ D1-backed SASL account store**, config-driven server identity, and a
21
+ **Node 24 / pnpm 11** toolchain. Phase 7 (TLS hardening & raw TCP) is
22
+ complete.
23
+
24
+ ### Added — Dual transport: WebSocket + `irc+tls :6697`
25
+
26
+ The wss-only transport decision (ADR-005) is superseded by **ADR-009**
27
+ (dual transport). The shared `ConnectionActor` now drives either
28
+ transport through a generalized seam; the parser/reducer/dispatch
29
+ pipeline is unchanged.
30
+
31
+ - **Transport seam** (`@serverless-ircd/irc-server`,
32
+ `@serverless-ircd/irc-core`): the `\r\n` line-framing is extracted into
33
+ a `Transport` abstraction. `ConnectionActor` accepts a
34
+ `WsTextFrameTransport` (one message per WS frame, prior behavior) or a
35
+ `TcpByteStreamTransport` (stateful `\r\n` framing across chunks with
36
+ partial-line buffering). Both produce identical reducer invocations.
37
+ - **Cloudflare TCP+TLS** (`@serverless-ircd/cf-tcp-container`): a new
38
+ workspace app — **Cloudflare Spectrum** terminates TLS at the edge and
39
+ forwards plaintext TCP to a stateful **Container** origin running the
40
+ IRC core. Ships a `Dockerfile`, config loader, deploy pipeline
41
+ (`.github/workflows/deploy-cf-tcp.yml`), and root
42
+ `deploy:cf-tcp:staging` / `deploy:cf-tcp:prod` scripts.
43
+ - **AWS TCP+TLS** (`@serverless-ircd/aws-adapter`, `@serverless-ircd/aws-stack`):
44
+ a **Network Load Balancer** with a `tls` listener terminates TLS and
45
+ invokes a **Lambda streaming function** (`nlb-stream.ts`) as the target.
46
+ Connection identity is derived from NLB flow metadata; bidirectional
47
+ traffic flows over the Lambda response stream, reusing `AwsRuntime`.
48
+ CDK constructs for NLB + target group + ACM cert are added.
49
+ - **Transport-parametrized contract suite** (`@serverless-ircd/irc-test-support`):
50
+ every scenario now runs across a `Transport` dimension
51
+ (in-memory+WS, in-memory+TCP, …), with TCP-specific framing scenarios
52
+ (chunk boundaries, mixed `\r\n`/`\n` line endings, partial sends).
53
+ - **Docs**: `docs/Cloudflare-TCP-Deployment.md` and
54
+ `docs/AWS-TCP-Deployment.md` — end-to-end :6697 deployment guides
55
+ (prerequisites, mTLS trust-store setup, cost/ops caveats).
56
+
57
+ ### Added — SASL `EXTERNAL` via mTLS (`@serverless-ircd/irc-core`, adapters)
58
+
59
+ - **mTLS support**: an `MtlsIdentityProvider` port in `irc-core` feeds
60
+ the verified client-certificate subject into `AccountStore`. Cloudflare
61
+ uses API Shield mTLS (uploaded CA pool); AWS uses API Gateway
62
+ custom-domain mTLS (PEM trust store, subject in the `$connect` event).
63
+ Identity is plumbed via a port — no cloud deps in `irc-core`.
64
+ - **`EXTERNAL` now authenticates** (was a stubbed `904 ERR_SASLFAIL` in
65
+ v0.2.0): `AUTHENTICATE EXTERNAL` against a bound provider succeeds with
66
+ `900 RPL_LOGGEDIN` / `903 RPL_SASLSUCCESS` when the cert maps to an
67
+ account.
68
+ - **mTLS-conditional advertisement**: the `sasl` cap value and
69
+ `908 ERR_SASLMECHS` are now driven by whether an mTLS provider is bound
70
+ — `PLAIN` only by default, `PLAIN,EXTERNAL` when one is. `AUTHENTICATE
71
+ EXTERNAL` without mTLS returns `908` (mechanism not available) instead
72
+ of the misleading `904`. Operators enable EXTERNAL by configuring edge
73
+ mTLS — no code change required.
74
+
75
+ ### Added — Remaining IRC query verbs (`@serverless-ircd/irc-core`)
76
+
77
+ - **`WHOWAS`**: a nick-history store (port + in-memory reference impl)
78
+ records `(nick, connId, username, hostname, signoffTime)` on QUIT /
79
+ NICK-change and purges past a TTL. `WHOWAS <nick>{,<nick>} [<count>]`
80
+ queries it — `314 RPL_WHOWASUSER` per match, `369 RPL_ENDOFWHOWAS`
81
+ terminator, `406 ERR_WASNOSUCHNICK` when no match.
82
+ - **`VERSION`** (`351`), **`TIME`** (`391`, uses the injected `Clock`),
83
+ **`ADMIN`** (`256`/`257`/`258`), **`INFO`** (`371`/`374`),
84
+ **`USERHOST`** (`302`), **`ISON`** (`303`) — each a reducer + a route
85
+ case with spec-correct numerics. Completes the standard query-verb set.
86
+
87
+ ### Added — Server identity from config (`@serverless-ircd/irc-core`)
88
+
89
+ - The `002` / `003` / `004` registration replies now reflect
90
+ `ServerConfig.serverVersion` / `createdAt` — the hardcoded
91
+ `'0.1.0'` / `'Phase 1'` placeholders are gone. A build step
92
+ (`scripts/generate-build-info.mjs`) bakes the package version and a
93
+ build timestamp into `RPL_CREATED` (`003`) when the config omits them.
94
+
95
+ ### Added — Cloudflare D1-backed SASL account store (`@serverless-ircd/cf-adapter`, `@serverless-ircd/irc-core`)
96
+
97
+ - Brings the CF adapter to parity with the AWS `DynamoAccountStore`. A
98
+ persistent `AccountStore` pre-loads scrypt-hashed rows from a new
99
+ **D1** database (`ACCOUNTS_DB` binding) at `ConnectionDO`
100
+ construction.
101
+ - **Shared scrypt hashing**: `hashAccountCredential` /
102
+ `verifyHashedPassword` are extracted to `irc-core` so both adapters use
103
+ one implementation (never plaintext).
104
+ - **Table-then-env precedence**: when the D1 table has rows it is
105
+ authoritative; when empty or unreachable the adapter falls back to the
106
+ `SASL_ACCOUNTS` env-var seed, preserving current behavior.
107
+ - **`tools/seed-cf-accounts.ts`**: provisioning CLI that scrypt-hashes
108
+ `username:password` pairs into the D1 `accounts` table (re-run
109
+ overwrites the prior row).
110
+
111
+ ### Added — AWS max-clients admission (`@serverless-ircd/aws-adapter`)
112
+
113
+ - `$connect` now enforces `serverConfig.maxClients` (was a reserved
114
+ no-op — `void params.serverConfig;`). Connections over the cap are
115
+ rejected at accept time, matching the Cloudflare admission gate.
116
+
117
+ ### Changed
118
+
119
+ - **Toolchain**: bumped from Node ≥ 20 / pnpm 9 to **Node ≥ 24 / pnpm
120
+ 11** (aligns the Lambda and Cloudflare Workers runtimes). Re-run
121
+ `corepack enable` to pick up the pinned pnpm.
122
+ - `@serverless-ircd/irc-core`: the request/response lookup `Effect`
123
+ variants (`LookupNick`, `GetConnectionInfo`, `GetChannelSnapshot`) are
124
+ removed from `effects.ts` and `dispatch` — they were dead scaffolding
125
+ with no producer or consumer.
126
+ - `@serverless-ircd/cf-adapter`: the dead channel-registration loop in
127
+ `ConnectionDO.webSocketMessage` and the reserved outbound-batching
128
+ field are removed (the latter was a never-wired optimization).
129
+
130
+ ### ⚠️ Migration required
131
+
132
+ - **Cloudflare: new D1 binding for SASL accounts.** To persist SASL
133
+ accounts, create the D1 database and add the `ACCOUNTS_DB` binding
134
+ (default + staging) to `wrangler.toml`:
135
+ ```bash
136
+ wrangler d1 create serverless-ircd-accounts
137
+ wrangler d1 execute serverless-ircd-accounts --command \
138
+ "CREATE TABLE IF NOT EXISTS accounts (account TEXT PRIMARY KEY, algorithm TEXT NOT NULL, salt TEXT NOT NULL, hash TEXT NOT NULL)"
139
+ ```
140
+ **No action is required to keep working** — when the D1 table is empty
141
+ or unreachable the adapter falls back to the `SASL_ACCOUNTS` env-var
142
+ seed. There is **no Durable-Object persisted-state schema change**
143
+ (`PERSISTED_STATE_VERSION` stays `1`); code rollback across this
144
+ release is safe.
145
+ - **Local toolchain**: Node ≥ 24 / pnpm 11 is now required. Re-run
146
+ `corepack enable`.
147
+
148
+ ### Known limitations
149
+
150
+ - The **CF TCP path requires Cloudflare Spectrum (Enterprise tier)** plus
151
+ a stateful Container origin; the **AWS TCP path** uses NLB + Lambda
152
+ streaming and is subject to Lambda idle-timeout / stream-duration
153
+ limits. The WebSocket path remains the zero-extra-deps default.
154
+ - **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
155
+ CA/trust-store** on both platforms and is not enabled by default.
156
+ - Oper-gated verbs (`KILL`, `REHASH`, `CONNECT`, `SQUIT`, `TRACE`,
157
+ `WALLOPS`, `SETNAME`, …) still return `421 ERR_UNKNOWNCOMMAND`.
158
+ - `serverName` still defaults to the `irc.example.com` placeholder when
159
+ an adapter config omits it (pending).
160
+
161
+ ### Security
162
+
163
+ - SASL `EXTERNAL` no longer returns a misleading `904 ERR_SASLFAIL` when
164
+ mTLS isn't configured — it returns `908 ERR_SASLMECHS` and the
165
+ mechanism is not advertised until a trust store is bound. SASL
166
+ credentials remain scrypt-hashed only (never plaintext), now via a
167
+ single shared implementation across both adapters.
168
+
169
+ ---
170
+
171
+ ## [0.3.0] - 2026-07-25
172
+
173
+ ### Added — Operator authentication (`@serverless-ircd/irc-core`)
174
+
175
+ - **`OPER` command reducer**: `OPER <name> <password>` authenticates the
176
+ caller against the deployment's configured operator credentials
177
+ (`OperCred[]`) and, on success, grants the `o` user mode, replying
178
+ `381 RPL_YOUREOPER`. Missing parameters → `461 ERR_NEEDMOREPARAMS`;
179
+ no credentials configured → `491 ERR_NOOPERHOST`; wrong name/password
180
+ → `464 ERR_PASSWDMISMATCH`. Already an operator is an idempotent
181
+ no-op that re-confirms with `381`. `OPER` is the *only* path to oper
182
+ — user `MODE +o` is hard-rejected with `481 ERR_NOPRIVILEGES` — and
183
+ unlocks the `313 RPL_WHOISOPERATOR` branch of WHOIS and the `*`
184
+ oper flag of WHO.
185
+
186
+ ### Added — IRCv3 `TAGMSG` (`@serverless-ircd/irc-core`)
187
+
188
+ - **`TAGMSG` reducer** (message-tags extension): carries tags only, no
189
+ text body. Gated behind the `message-tags` capability (clients that
190
+ did not negotiate it see `421 ERR_UNKNOWNCOMMAND`). Follows NOTICE
191
+ semantics — no numeric error replies on any rejection path — for both
192
+ channel and user targets, and is recorded so `draft/chathistory`
193
+ replay picks up `TAGMSG` lines.
194
+
195
+ ### Added — RFC 1459 case-mapping (`@serverless-ircd/irc-core`)
196
+
197
+ - **`case-fold.ts`**: RFC 1459 case-insensitive comparison for nicks
198
+ and channels, advertised via `005 CASEMAPPING=rfc1459`. Replaces the
199
+ prior ASCII-only comparison across nick collision/registry, kick and
200
+ invite target lookup, and membership matching.
201
+
202
+ ### Added — Registration MOTD (`@serverless-ircd/irc-core`)
203
+
204
+ - The registration welcome now renders the **real configured MOTD**
205
+ (`motd-lines.ts`), replacing the placeholder banner. The `MOTD`
206
+ command and the welcome flow share one rendering path.
207
+
208
+ ### Added — Cloudflare adapter (`@serverless-ircd/cf-adapter`)
209
+
210
+ - **`ChannelRegistryDO`**: a fourth Durable Object holding the channel
211
+ directory (channel name → `ChannelDO` routing), alongside
212
+ `ConnectionDO`, `ChannelDO`, and `RegistryDO`. Wired into the Worker
213
+ exports, bindings, and SQLite migrations.
214
+ - **Cloudflare config loader**: reduces Workers `vars` to the shared
215
+ `ServerConfigSchema`, matching the AWS (`loadServerConfigFromLambdaEnv`)
216
+ and local-CLI loaders.
217
+ - `CfRuntime` completed — the previously stubbed `IrcRuntime` methods
218
+ (`getConnection`, `getChannelConnections`, `listChannels`, and
219
+ cross-connection `disconnect`) are now implemented.
220
+
221
+ ### Added — SASL account persistence (`@serverless-ircd/aws-adapter`)
222
+
223
+ - SASL `PLAIN` accounts are now persisted in the DynamoDB `Accounts`
224
+ table with **scrypt** password hashes (never plaintext). The Lambda
225
+ cold-start path scans the table once and loads credentials into the
226
+ synchronous `AccountStore` port.
227
+ - **`tools/seed-aws-accounts.ts`**: provisioning script that scrypt-hashes
228
+ `username:password` pairs (from CLI args or a file) and writes them
229
+ to the `Accounts` table. Re-running with the same username overwrites
230
+ the prior row.
231
+
232
+ ### Changed
233
+
234
+ - `@serverless-ircd/in-memory-runtime`: `admitConnection` now returns a
235
+ stable empty `recordId` when admission is disabled, dropping the
236
+ per-call id minting (the id was never consulted).
237
+ - Deploy scripts: root `pnpm deploy:aws:staging` / `deploy:aws:prod`
238
+ now run `pnpm build` before `cdk deploy`, so the esbuild-bundled
239
+ Lambda picks up fresh workspace `dist/` — matching what CI already
240
+ did and fixing local deploys shipping stale code.
241
+
242
+ ### Known limitations
243
+
244
+ - SASL `EXTERNAL` (mTLS) and production TLS transport remain pending
245
+ (0.x work).
246
+ - Deployed stacks remain WebSocket-only; stock TCP IRC clients still
247
+ reach them through the local `tcp-ws-forwarder` bridge.
248
+
249
+ ### Security
250
+
251
+ - None. No vulnerabilities have been reported or fixed in this release.
252
+
253
+ ---
254
+
13
255
  ## [0.2.0] - 2026-07-23
14
256
 
15
257
  ### Added — Authentication & flood control (`@serverless-ircd/irc-core`)
@@ -294,31 +536,29 @@ manual per `docs/release.md` §8.3.
294
536
 
295
537
  ### Known limitations
296
538
 
297
- - **No AWS adapter.** Tickets TICKET-039 through TICKET-044 (CDK
298
- stack, APIGW WebSocket handlers, DynamoDB transactions, EventBridge
299
- timers, deploy pipeline, deployment doc) are pending. Cloudflare is
300
- the only deployable platform in 0.1.0.
539
+ - **No AWS adapter.** The CDK stack, APIGW WebSocket handlers, DynamoDB
540
+ transactions, EventBridge timers, deploy pipeline, and deployment doc
541
+ are pending. Cloudflare is the only deployable platform in 0.1.0.
301
542
  - **No SASL.** `CAP sasl` is advertised in the capability list but
302
543
  `AUTHENTICATE` is not wired into the dispatcher. Plain-text password
303
- authentication via `PASS` is also not enforced (TICKET-046).
304
- - **No flood control.** A token-bucket wrapper (TICKET-028) is
305
- pending. Misbehaving clients can send unbounded commands.
544
+ authentication via `PASS` is also not enforced.
545
+ - **No flood control.** A token-bucket wrapper is pending. Misbehaving
546
+ clients can send unbounded commands.
306
547
  - **No server-password enforcement, rate limits, cloaking, or oper
307
- credentials** (TICKET-046).
548
+ credentials.**
308
549
  - **No config loader.** Server name, network name, MOTD, channel
309
- prefixes, max clients, oper creds are hardcoded defaults
310
- (TICKET-047). The `MOTD.txt` file at the repo root is informational
311
- only; the live MOTD is compiled into the Worker from
312
- `[vars].MOTD_LINES`.
550
+ prefixes, max clients, oper creds are hardcoded defaults. The
551
+ `MOTD.txt` file at the repo root is informational only; the live MOTD
552
+ is compiled into the Worker from `[vars].MOTD_LINES`.
313
553
  - **No CI coverage gate.** Coverage is computed and uploaded as an
314
- artifact, but no threshold is enforced (TICKET-048). Inspect
554
+ artifact, but no threshold is enforced. Inspect
315
555
  `packages/*/coverage/` before deploying.
316
556
  - **No TLS / raw TCP transport.** v1 is WebSocket text frames only.
317
- TLS-hardened and `irc+tls` transports are 0.x work (TICKET-052
318
- through TICKET-058). Stock TCP IRC clients reach the server through
319
- the local `tcp-ws-forwarder` bridge.
320
- - **No `chathistory` playback.** TICKET-063 is pending; clients that
321
- request backlog on JOIN will see an empty batch or no response.
557
+ TLS-hardened and `irc+tls` transports are 0.x work. Stock TCP IRC
558
+ clients reach the server through the local `tcp-ws-forwarder` bridge.
559
+ - **No `chathistory` playback.** Chat-history playback is pending;
560
+ clients that request backlog on JOIN will see an empty batch or no
561
+ response.
322
562
 
323
563
  ### Security
324
564
 
package/README.md CHANGED
@@ -4,18 +4,23 @@ A serverless IRC daemon where the IRC protocol logic lives in a pure,
4
4
  platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
5
5
  (Durable Objects) or **AWS** (API Gateway WebSockets + Lambda + DynamoDB).
6
6
 
7
- One TypeScript codebase. Two serverless substrates. No S2S linking in v1.
7
+ One TypeScript codebase. Two serverless substrates.
8
8
 
9
- > **Status:** **v0.2.0 (preview).** The pure protocol core, the
9
+ > **Status:** **v0.4.0 (preview).** The pure protocol core, the
10
10
  > `IrcRuntime` port + in-memory runtime, a runnable local CLI server,
11
11
  > the **Cloudflare Workers** adapter, and the **AWS** (API Gateway
12
12
  > WebSocket + Lambda + DynamoDB + CDK) adapter are all functional and
13
- > deployed to staging. SASL (`PLAIN`), token-bucket flood control,
14
- > server-password enforcement, hostmask cloaking, connection admission,
15
- > IRCv3 chat history, and a CI coverage + mutation-testing gate landed
16
- > in v0.2.0. Remaining 0.x work: oper credentials /
17
- > config loader, SASL `EXTERNAL` (mTLS), and production TLS transport.
18
- > See `CHANGELOG.md` for the per-release manifests.
13
+ > deployed to staging. v0.4.0 ships **dual transport**: both adapters
14
+ > now speak a real `irc+tls :6697` (TLS-over-TCP) surface for stock IRC
15
+ > clients Cloudflare via Spectrum + a Container origin, AWS via a
16
+ > Network Load Balancer + Lambda streaming alongside the WebSocket
17
+ > path. It also lands **SASL `EXTERNAL` via mTLS**, the remaining
18
+ > standard IRC verbs (`WHOWAS`, `VERSION`, `TIME`, `ADMIN`, `INFO`,
19
+ > `USERHOST`, `ISON`), a **Cloudflare D1-backed SASL account store**,
20
+ > config-driven server identity, and a **Node 24 / pnpm 11** toolchain.
21
+ > Remaining 0.x work: oper-gated verbs (`KILL`/`REHASH`/…), a
22
+ > required-`serverName` config gate, and load/compat testing. See
23
+ > `CHANGELOG.md` for the per-release manifests.
19
24
 
20
25
  ---
21
26
 
@@ -57,14 +62,25 @@ Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
57
62
  │ packages/cf-adapter │ │ packages/aws-adapter │
58
63
  │ ConnectionDO │ │ $connect/$disconnect/ │
59
64
  │ ChannelDO │ │ $default Lambda handlers │
60
- RegistryDO │ │ DynamoDB tables │
61
- CfRuntime │ │ AwsRuntime │
65
+ ChannelRegistryDO │ │ DynamoDB tables │
66
+ RegistryDO │ │ AwsRuntime │
67
+ │ CfRuntime │ │ │
62
68
  └────────────────────────────┘ └──────────────────────────────┘
63
69
  ```
64
70
 
65
71
  `packages/in-memory-runtime` is the reference implementation of
66
72
  `IrcRuntime`, used by integration tests and the local CLI server.
67
73
 
74
+ **Two transports, one core.** Both adapters accept WebSocket text frames
75
+ (the serverless default) **and** a raw `irc+tls :6697` (TLS-over-TCP)
76
+ surface for stock IRC clients. The TCP+TLS edge is platform-specific —
77
+ Cloudflare **Spectrum** terminates TLS and forwards plaintext TCP to a
78
+ stateful **Container** origin (`apps/cf-tcp-container`); AWS terminates
79
+ TLS at a **Network Load Balancer** and invokes a **Lambda streaming**
80
+ function. Both feed the same `ConnectionActor` through a `Transport`
81
+ seam (`WsTextFrameTransport` vs. `TcpByteStreamTransport`); the
82
+ parser/reducer/dispatch pipeline is identical.
83
+
68
84
  ### The key idea: pure reducers + location-of-authority
69
85
 
70
86
  Each command handler is a pure function:
@@ -106,11 +122,14 @@ ServerlessIRCd/
106
122
  │ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime
107
123
  ├── apps/
108
124
  │ ├── cf-worker/ worker entry, DO migrations, bindings
109
- │ ├── aws-stack/ CDK stack (APIGW WS + Lambda + DynamoDB)
125
+ │ ├── cf-tcp-container/ Spectrum + Container origin for irc+tls :6697
126
+ │ ├── aws-stack/ CDK stack (APIGW WS + NLB + Lambda streaming + DynamoDB)
110
127
  │ └── local-cli/ runnable WS + TCP server using in-memory-runtime
111
128
  ├── tools/
112
129
  │ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
113
- └── ci-hardening/ coverage-gate + mutation-config validators
130
+ ├── ci-hardening/ coverage-gate + mutation-config validators
131
+ │ ├── seed-aws-accounts.ts scrypt-hash SASL PLAIN accounts into DynamoDB
132
+ │ └── seed-cf-accounts.ts scrypt-hash SASL PLAIN accounts into Cloudflare D1
114
133
  ├── pnpm-workspace.yaml turbo.json tsconfig.base.json
115
134
  └── README.md CHANGELOG.md
116
135
  ```
@@ -121,7 +140,7 @@ ServerlessIRCd/
121
140
 
122
141
  | Concern | Choice |
123
142
  |------------------|---------------------------------------------------|
124
- | Runtime | Node ≥ 20, ES2022+ |
143
+ | Runtime | Node ≥ 24, ES2022+ |
125
144
  | Package mgr | pnpm workspaces |
126
145
  | Build cache | turbo |
127
146
  | Language | TypeScript (strict, `exactOptionalPropertyTypes`) |
@@ -135,9 +154,12 @@ ServerlessIRCd/
135
154
 
136
155
  ## Getting started
137
156
 
138
- Requires Node ≥ 20 and pnpm 9.
157
+ Requires Node ≥ 24 and pnpm 11. Enable corepack so the `packageManager`
158
+ pin is resolved automatically:
139
159
 
140
160
  ```bash
161
+ corepack enable # lets the pinned pnpm@11 run on any Node ≥ 24
162
+
141
163
  pnpm install # install workspace deps
142
164
 
143
165
  pnpm build # build all packages (turbo)
@@ -316,10 +338,16 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
316
338
  - **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
317
339
  ✅ landed in **v0.2.0** (staging auto-deploy via CI; prod deploy is manual).
318
340
  - **Phase 5** — Observability, security hardening, config, CI gates.
319
- mostly landed (logger port, server-password, cloaking, admission
320
- limits, coverage + mutation gates). Remaining: oper credentials / config
321
- loader.
322
- - **Phase 6** — Load testing, client compatibility sweep, ADRs.
341
+ ✅ landed across v0.2.0–v0.4.0 (logger port, server-password, cloaking,
342
+ admission limits, coverage + mutation gates, OPER credential auth, the
343
+ Cloudflare config loader, and AWS max-clients admission).
344
+ - **Phase 6** — TLS hardening & raw TCP transport (`irc+tls :6697`).
345
+ ✅ landed in **v0.4.0**: generalized `ConnectionActor` transport seam,
346
+ Cloudflare Spectrum + Container origin, AWS NLB + Lambda streaming,
347
+ mTLS → SASL `EXTERNAL`, transport-parametrized contract suite, ADR-009.
348
+ - **Phase 7** — Load testing (10k concurrent connections per platform),
349
+ client compatibility sweep (WeeChat, HexChat, IRCCloud, TheLounge),
350
+ remaining ADRs.
323
351
 
324
352
  ---
325
353
 
@@ -327,23 +355,31 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
327
355
 
328
356
  **Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
329
357
  `PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
330
- channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD`,
331
- `AWAY`.
358
+ channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`,
359
+ `WHOWAS`, `MOTD`, `AWAY`, `OPER` (credential auth → `o` user mode), plus
360
+ the query verbs `VERSION`, `TIME`, `ADMIN`, `INFO`, `USERHOST`, `ISON`.
361
+ (Oper-gated verbs — `KILL`/`REHASH`/`CONNECT`/`SQUIT`/`TRACE` — remain
362
+ deferred.)
332
363
 
333
364
  **Channel modes:** `o v b i k l t n m s p`.
334
365
  **User modes:** `i`, `o` (local only), `w`, `s`.
335
366
 
336
- **IRCv3 extensions (negotiated via `CAP`):** `message-tags`, `server-time`,
337
- `account-tag`, `echo-message`, `batch`, `sasl` (`PLAIN` implemented,
338
- `EXTERNAL` reserved pending mTLS), `multi-prefix`, `away-notify`, `chghost`,
339
- `invite-notify`, `extended-join`, `draft/chathistory`, `safelist`.
367
+ **IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
368
+ `TAGMSG` command), `server-time`, `account-tag`, `echo-message`, `batch`,
369
+ `sasl` (`PLAIN` always; `EXTERNAL` via mTLS when a client-cert trust store
370
+ is bound),
371
+ `multi-prefix`, `away-notify`, `chghost`, `invite-notify`, `extended-join`,
372
+ `draft/chathistory`, `safelist`. Case-insensitive nick/channel comparison
373
+ uses **RFC 1459** case-mapping (advertised via `005 CASEMAPPING=rfc1459`).
340
374
 
341
375
  **Transport:** deployed stacks (Cloudflare Workers, AWS API Gateway) speak
342
- WebSocket text frames only (one IRC message per frame, with tolerance for
343
- `\r\n`-joined frames). The local CLI additionally binds a plain RFC TCP
344
- listener for direct IRC-client access. Stock TCP IRC clients reach a deployed
345
- WebSocket endpoint through the local
346
- [`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder).
376
+ WebSocket text frames by default (one IRC message per frame, with tolerance
377
+ for `\r\n`-joined frames), and optionally expose a raw `irc+tls :6697`
378
+ (TLS-over-TCP) surface Cloudflare via **Spectrum + Container**, AWS via a
379
+ **Network Load Balancer + Lambda streaming** function. The local CLI
380
+ additionally binds a plain RFC TCP listener for direct IRC-client access,
381
+ and the [`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder)
382
+ bridges a stock TCP client to a deployed WebSocket endpoint.
347
383
 
348
384
  ---
349
385
 
@@ -351,7 +387,13 @@ WebSocket endpoint through the local
351
387
 
352
388
  - `CHANGELOG.md` — per-release manifests (Keep a Changelog format), including
353
389
  the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
354
- gates).
390
+ gates), v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
391
+ ChannelRegistryDO, SASL account persistence), and v0.4.0 (dual transport
392
+ `irc+tls :6697`, SASL `EXTERNAL`/mTLS, WHOWAS + remaining verbs, D1 account
393
+ store, Node 24/pnpm 11).
394
+ - `docs/Cloudflare-TCP-Deployment.md` and `docs/AWS-TCP-Deployment.md` —
395
+ end-to-end guides for the `:6697` TCP+TLS variants (Spectrum/Container on
396
+ CF, NLB + Lambda streaming on AWS), including mTLS trust-store setup.
355
397
  - `README.md` in each `packages/*` and `apps/*` — per-package notes (e.g.
356
398
  `packages/aws-adapter/README.md` for the DynamoDB-Local test setup,
357
399
  `apps/aws-stack/README.md` for CDK commands and localstack validation).
@@ -1,7 +1,7 @@
1
1
  # `@serverless-ircd/aws-stack`
2
2
 
3
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
4
+ (PLAN §6.2): an API Gateway v2 WebSocket API, a single Node 24 Lambda wired
5
5
  to the `$connect` / `$disconnect` / `$default` routes, five DynamoDB tables
6
6
  (`Connections`, `ChannelMeta`, `ChannelMembers`, `Nicks`, `Accounts`), and the
7
7
  least-privilege IAM glue to let the Lambda read/write the tables and post back
@@ -12,7 +12,7 @@ The Lambda handler is currently a stub (`{ statusCode: 200 }`); the real
12
12
 
13
13
  ## Prerequisites
14
14
 
15
- - Node.js 20+
15
+ - Node.js 24+
16
16
  - pnpm (workspace root installs this package)
17
17
  - An AWS account with credentials configured
18
18
  (`aws configure`, `AWS_PROFILE`, or `SSO`)
@@ -38,11 +38,18 @@ const motdLines = Array.isArray(rawMotd)
38
38
  ? rawMotd.split('\n')
39
39
  : undefined;
40
40
 
41
+ // TCP+TLS (irc+tls :6697) is opt-in: only provisioned when this context
42
+ // var is supplied. When omitted, only the wss (API Gateway WebSocket)
43
+ // path deploys (the default documented in docs/AWS-Deployment.md). See
44
+ // docs/AWS-TCP-Deployment.md.
45
+ const tcpTlsDomainName = app.node.tryGetContext('tcpTlsDomainName') as string | undefined;
46
+
41
47
  const stackProps: IrcStackProps = {
42
48
  environmentName,
43
49
  ...(serverName !== undefined ? { serverName } : {}),
44
50
  ...(networkName !== undefined ? { networkName } : {}),
45
51
  ...(motdLines !== undefined ? { motdLines } : {}),
52
+ ...(tcpTlsDomainName !== undefined ? { tcpTlsDomainName } : {}),
46
53
  ...(account && region ? { env: { account, region } } : {}),
47
54
  };
48
55