serverless-ircd 0.3.0 → 0.5.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 (223) 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 +349 -18
  8. package/README.md +132 -30
  9. package/apps/aws-stack/README.md +6 -5
  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 +150 -10
  13. package/apps/aws-stack/tests/stack.test.ts +145 -4
  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/wrangler.test.toml +6 -0
  34. package/apps/cf-worker/wrangler.toml +28 -2
  35. package/apps/local-cli/package.json +1 -1
  36. package/apps/local-cli/src/config-loader.ts +10 -0
  37. package/apps/local-cli/src/server.ts +149 -24
  38. package/apps/local-cli/tests/e2e.test.ts +1 -1
  39. package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
  40. package/package.json +14 -10
  41. package/packages/aws-adapter/package.json +3 -3
  42. package/packages/aws-adapter/src/account-store.ts +1 -1
  43. package/packages/aws-adapter/src/admission.ts +74 -0
  44. package/packages/aws-adapter/src/aws-runtime.ts +75 -4
  45. package/packages/aws-adapter/src/config-loader.ts +32 -0
  46. package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
  47. package/packages/aws-adapter/src/handlers/connect.ts +96 -9
  48. package/packages/aws-adapter/src/handlers/default.ts +98 -7
  49. package/packages/aws-adapter/src/handlers/index.ts +109 -4
  50. package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
  51. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  52. package/packages/aws-adapter/src/index.ts +8 -0
  53. package/packages/aws-adapter/src/serialize.ts +11 -1
  54. package/packages/aws-adapter/src/stats.ts +80 -0
  55. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
  56. package/packages/aws-adapter/tests/account-store.test.ts +19 -20
  57. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  58. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  59. package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
  60. package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
  61. package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
  62. package/packages/aws-adapter/tests/connect.test.ts +174 -0
  63. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
  64. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  65. package/packages/aws-adapter/tests/handlers.test.ts +302 -53
  66. package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
  67. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  68. package/packages/aws-adapter/tests/stats.test.ts +317 -0
  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 +5 -1
  72. package/packages/cf-adapter/src/cf-runtime.ts +68 -5
  73. package/packages/cf-adapter/src/channel-do.ts +2 -2
  74. package/packages/cf-adapter/src/config-loader.ts +33 -0
  75. package/packages/cf-adapter/src/connection-do.ts +278 -85
  76. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  77. package/packages/cf-adapter/src/env.ts +54 -11
  78. package/packages/cf-adapter/src/index.ts +11 -8
  79. package/packages/cf-adapter/src/registry-do.ts +22 -3
  80. package/packages/cf-adapter/src/sharding.ts +1 -2
  81. package/packages/cf-adapter/src/stats.ts +65 -0
  82. package/packages/cf-adapter/tests/cf-harness.ts +12 -2
  83. package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
  84. package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
  85. package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
  86. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  87. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  88. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  89. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  90. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
  91. package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
  92. package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
  93. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  94. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  95. package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
  96. package/packages/cf-adapter/tests/sharding.test.ts +1 -1
  97. package/packages/cf-adapter/tests/stats.test.ts +120 -0
  98. package/packages/cf-adapter/tests/worker/main.ts +15 -8
  99. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
  100. package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
  101. package/packages/cf-adapter/wrangler.test.toml +15 -0
  102. package/packages/in-memory-runtime/package.json +1 -1
  103. package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
  104. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
  105. package/packages/irc-core/package.json +6 -1
  106. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  107. package/packages/irc-core/src/admission.ts +16 -15
  108. package/packages/irc-core/src/caps/capabilities.ts +24 -3
  109. package/packages/irc-core/src/cloak.ts +1 -1
  110. package/packages/irc-core/src/commands/cap.ts +8 -1
  111. package/packages/irc-core/src/commands/index.ts +17 -0
  112. package/packages/irc-core/src/commands/invite.ts +2 -4
  113. package/packages/irc-core/src/commands/ison.ts +61 -0
  114. package/packages/irc-core/src/commands/isupport.ts +6 -2
  115. package/packages/irc-core/src/commands/kick.ts +2 -4
  116. package/packages/irc-core/src/commands/kill.ts +127 -0
  117. package/packages/irc-core/src/commands/list.ts +1 -1
  118. package/packages/irc-core/src/commands/lusers.ts +204 -0
  119. package/packages/irc-core/src/commands/mode.ts +4 -8
  120. package/packages/irc-core/src/commands/names.ts +3 -5
  121. package/packages/irc-core/src/commands/part.ts +2 -4
  122. package/packages/irc-core/src/commands/quit.ts +12 -0
  123. package/packages/irc-core/src/commands/registration.ts +18 -12
  124. package/packages/irc-core/src/commands/rehash.ts +119 -0
  125. package/packages/irc-core/src/commands/sasl.ts +72 -9
  126. package/packages/irc-core/src/commands/server-info.ts +129 -0
  127. package/packages/irc-core/src/commands/setname.ts +109 -0
  128. package/packages/irc-core/src/commands/stats.ts +152 -0
  129. package/packages/irc-core/src/commands/topic.ts +2 -4
  130. package/packages/irc-core/src/commands/trace.ts +137 -0
  131. package/packages/irc-core/src/commands/userhost.ts +84 -0
  132. package/packages/irc-core/src/commands/wallops.ts +118 -0
  133. package/packages/irc-core/src/commands/whowas.ts +113 -0
  134. package/packages/irc-core/src/config.ts +65 -0
  135. package/packages/irc-core/src/credential-hashing.ts +124 -0
  136. package/packages/irc-core/src/effects.ts +33 -30
  137. package/packages/irc-core/src/index.ts +3 -0
  138. package/packages/irc-core/src/ports.ts +360 -12
  139. package/packages/irc-core/src/protocol/numerics.ts +48 -11
  140. package/packages/irc-core/src/protocol/outbound.ts +20 -3
  141. package/packages/irc-core/src/types.ts +46 -2
  142. package/packages/irc-core/src/ws-framing.ts +132 -0
  143. package/packages/irc-core/src/ws-subprotocol.ts +66 -0
  144. package/packages/irc-core/tests/account-store.test.ts +45 -2
  145. package/packages/irc-core/tests/admission.test.ts +18 -0
  146. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  147. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  148. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  149. package/packages/irc-core/tests/commands/kill.test.ts +243 -0
  150. package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
  151. package/packages/irc-core/tests/commands/mode.test.ts +57 -0
  152. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  153. package/packages/irc-core/tests/commands/registration.test.ts +151 -6
  154. package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
  155. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  156. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  157. package/packages/irc-core/tests/commands/setname.test.ts +225 -0
  158. package/packages/irc-core/tests/commands/stats.test.ts +294 -0
  159. package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
  160. package/packages/irc-core/tests/commands/trace.test.ts +282 -0
  161. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  162. package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
  163. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  164. package/packages/irc-core/tests/config.test.ts +95 -1
  165. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  166. package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
  167. package/packages/irc-core/tests/effects.test.ts +14 -27
  168. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  169. package/packages/irc-core/tests/numerics.test.ts +102 -0
  170. package/packages/irc-core/tests/outbound.test.ts +51 -0
  171. package/packages/irc-core/tests/ports.test.ts +22 -0
  172. package/packages/irc-core/tests/raw-modules.d.ts +11 -0
  173. package/packages/irc-core/tests/stats-store.test.ts +222 -0
  174. package/packages/irc-core/tests/types.test.ts +35 -1
  175. package/packages/irc-core/tests/ws-framing.test.ts +213 -0
  176. package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
  177. package/packages/irc-core/tsconfig.build.json +1 -1
  178. package/packages/irc-core/tsconfig.test.json +1 -1
  179. package/packages/irc-server/package.json +1 -1
  180. package/packages/irc-server/src/actor.ts +393 -16
  181. package/packages/irc-server/src/dispatch.ts +1 -3
  182. package/packages/irc-server/src/index.ts +10 -2
  183. package/packages/irc-server/src/routing.ts +15 -0
  184. package/packages/irc-server/src/runtime.ts +31 -0
  185. package/packages/irc-server/src/transport.ts +104 -0
  186. package/packages/irc-server/tests/actor.test.ts +1489 -4
  187. package/packages/irc-server/tests/dispatch.test.ts +37 -17
  188. package/packages/irc-server/tests/raw-modules.d.ts +11 -0
  189. package/packages/irc-server/tests/routing.test.ts +5 -0
  190. package/packages/irc-server/tests/runtime.test.ts +7 -0
  191. package/packages/irc-server/tests/transport.test.ts +230 -0
  192. package/packages/irc-test-support/package.json +1 -1
  193. package/packages/irc-test-support/src/harness.ts +44 -9
  194. package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
  195. package/packages/irc-test-support/src/index.ts +3 -0
  196. package/packages/irc-test-support/src/scenarios.ts +141 -3
  197. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  198. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  199. package/pnpm-workspace.yaml +10 -1
  200. package/tools/ci-hardening/package.json +1 -1
  201. package/tools/package.json +9 -0
  202. package/tools/seed-aws-accounts.ts +5 -6
  203. package/tools/seed-cf-accounts.ts +107 -0
  204. package/tools/tcp-ws-forwarder/package.json +1 -1
  205. package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
  206. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
  207. package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
  208. package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
  209. package/docs/ADR-002-location-of-authority.md +0 -82
  210. package/docs/ADR-003-durable-object-sharding.md +0 -93
  211. package/docs/ADR-004-dynamodb-schema.md +0 -96
  212. package/docs/ADR-005-wss-only-transport-v1.md +0 -83
  213. package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
  214. package/docs/ADR-007-deterministic-ports.md +0 -82
  215. package/docs/ADR-008-monorepo-tooling.md +0 -60
  216. package/docs/AWS-Adapter-Architecture.md +0 -496
  217. package/docs/AWS-Deployment.md +0 -1186
  218. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  219. package/docs/Home.md +0 -11
  220. package/docs/Observability.md +0 -87
  221. package/docs/PlanIRCv3Websocket.md +0 -489
  222. package/docs/PlanWebClient.md +0 -451
  223. package/docs/Release-Process.md +0 -443
@@ -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,339 @@ 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.5.0] - 2026-08-02
14
+
15
+ The headline is **protocol completion**: the deferred IRC verbs land
16
+ (Phase 11), and the S2S / obsolete RFC 2812 verbs are **formally
17
+ dropped** rather than left "deferred". With this release every standard
18
+ IRC verb a modern client expects is implemented — the only remaining
19
+ `421 ERR_UNKNOWNCOMMAND` paths are the deliberately-unimplemented S2S
20
+ and obsolete verbs. The roadmap's 0.x protocol surface is complete;
21
+ what remains is the load/compat sweep (10k-connection load test +
22
+ client compatibility matrix) and the required-`serverName` config gate.
23
+
24
+ ### Added — Deferred IRC verbs (`@serverless-ircd/irc-core`, `@serverless-ircd/irc-server`)
25
+
26
+ Six Phase 11 tickets light up the remaining command surface. Each ships
27
+ a pure reducer, an actor route case (no more `421`), new numerics in
28
+ `protocol/numerics.ts`, and unit + actor coverage. (The remaining two
29
+ Phase 11 tickets are drops — see below.)
30
+
31
+ - **`KILL`** (oper-gated force-disconnect): `KILL <nick>
32
+ <comment>` resolves the target via the nick registry + a live
33
+ connection fetch (mirroring WHOIS), checks the oper gate (`481
34
+ ERR_NOPRIVILEGES`), and emits a `Disconnect` effect carrying the
35
+ classic `Killed (<oper> (<comment>))` reason. Peer `QUIT` fanout in
36
+ the target's shared channels happens via the runtime's existing
37
+ `disconnect` path. Missing params → `461`; unknown nick → `401`; an
38
+ oper may `KILL` themselves (RFC-permitted).
39
+ **Platform asymmetry:** on Cloudflare the `disconnect` RPC closes the
40
+ target DO's socket immediately; on AWS a `$default` Lambda invocation
41
+ cannot close a *remote* APIGW WebSocket, so the target's
42
+ live socket only closes on its next `$disconnect` / sweeper cycle —
43
+ the QUIT fanout to peers still fires. The reducer is identical; the
44
+ difference is the bound runtime's `disconnect` semantics.
45
+ - **`REHASH`** (oper-gated config reload): emits `382
46
+ RPL_REHASHING :<source>` and re-fetches `ServerConfig` from the
47
+ adapter's bound source via a new `IrcRuntime.reloadConfig()` port
48
+ method. The refreshed value swaps into the actor's live config
49
+ reference so subsequent commands observe it (rotated oper passwords,
50
+ new MOTD source, etc.). On reload failure the previous config stays
51
+ in effect and a graceful error-suffixed `382` follows — the oper is
52
+ **not** disconnected (a bad store read must never sever a live link).
53
+ Already-open connections keep their negotiated caps and registration;
54
+ transport/TLS reload requires a redeploy.
55
+ - **`LUSERS` + `STATS`** (network statistics): backed by a
56
+ new async `ServerStats` port (`getStats(): Promise<ServerStatsSnapshot>`)
57
+ and an `InMemoryStats` reference impl. `LUSERS` emits the full
58
+ `251`–`255` sequence (only the non-zero counts, per RFC) plus the
59
+ optional `265` / `266` local/global counts. `STATS <query>` supports
60
+ the `u` (uptime, `242 RPL_STATSUPTIME`) and `l` (link info,
61
+ `211 RPL_STATSLINKINFO`) letters; unknown letters get the
62
+ charybdis-style empty body terminated by `219 RPL_ENDOFSTATS`. A
63
+ `<server>` argument that does not match the local server (case-
64
+ insensitive) yields `402 ERR_NOSUCHSERVER` (single-server network —
65
+ S2S aggregation is a PLAN non-goal). The `computeStatsSnapshot`
66
+ helper centralizes the oper/invisible/unknown classification so each
67
+ adapter's backend only has to gather raw counts.
68
+ - **`TRACE`** (routing diagnostics): in a single-server
69
+ serverless IRCd `TRACE` collapses to a local-server line + the
70
+ `262 RPL_ENDOFTRACE` terminator. Per-connection `204
71
+ RPL_TRACEOPERATOR` / `205 RPL_TRACEUSER` detail is **oper-gated** —
72
+ non-opers see only the server line + `262` (no enumeration of `+i`
73
+ users they don't share a channel with). `TRACE <server>` where
74
+ `<server>` is not the local server → `402`.
75
+ - **`WALLOPS`** (oper broadcast): `WALLOPS :<message>` is
76
+ oper-only to send (modern ircd convention — non-oper → `481`);
77
+ delivery fans out to every connection whose `+w` user mode is set via
78
+ a new `BroadcastWallops` effect interpreted by the runtime's
79
+ `broadcastWallops` method (global cross-connection fanout, not
80
+ channel-scoped). The originator is always excluded (skip-self,
81
+ matching NOTICE/PRIVMSG semantics). Empty message → `461`. The `+w`
82
+ user mode was already accepted by `MODE` and advertised in `004
83
+ RPL_MYINFO`; this lights up the consumer.
84
+ - **`SETNAME`** (realname change): `SETNAME :<realname>`
85
+ updates `state.realname` so a subsequent `WHOIS` reflects the new
86
+ value in `311 RPL_WHOISUSER`. No broadcast is emitted in v1 (there
87
+ is no standard IRCv3 cap for relaying a realname change — a
88
+ `draft/setname` relay is a documented follow-up). Empty/missing
89
+ realname → `461`; the registration-time realname length cap applies.
90
+
91
+ ### Changed — S2S & obsolete verbs formally dropped (`@serverless-ircd/irc-core`)
92
+
93
+ The remaining two Phase 11 tickets convert the "deferred" framing into
94
+ an explicit **drop**. There is no behaviour change on the wire — the verbs
95
+ already returned `421 ERR_UNKNOWNCOMMAND` — but the framing, comments,
96
+ and reserved numerics are cleaned up so the codebase no longer carries
97
+ dead scaffolding.
98
+
99
+ - **S2S verbs** (`CONNECT`, `SQUIT`, `LINKS`): dropped as a PLAN
100
+ non-goal (single-server serverless IRCd; no mesh to connect, split,
101
+ or list). The now-unused numerics `RPL_LINKS (364)`,
102
+ `RPL_ENDOFLINKS (365)`, and `ERR_CANTKILLSERVER (483)` are removed
103
+ from `numerics.ts` and its auto-derived `numericToName` reverse map.
104
+ `ERR_NOSUCHSERVER (402)` is retained — `LUSERS` / `STATS` / `TRACE`
105
+ use it for remote-server gating.
106
+ - **Obsolete RFC 2812 verbs** (`SERVICE`, `SUMMON`, `USERS`): dropped
107
+ (never widely implemented; no modern client uses them). The now-unused
108
+ numerics `RPL_YOURESERVICE (383)`, `ERR_NOSUCHSERVICE (408)`,
109
+ `ERR_SUMMONDISABLED (445)`, and `ERR_USERSDISABLED (446)` are removed.
110
+ - The `actor.ts` default-branch triage comment was rewritten: the
111
+ six verbs are documented as **deliberately unimplemented** rather
112
+ than "deferred".
113
+ - Source-level regression guards
114
+ (`packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts`)
115
+ and a behaviour guard in `actor.test.ts` (the verbs still emit
116
+ `421`) pin the drop so the numerics or the "deferred" framing cannot
117
+ creep back in.
118
+
119
+ ### Added — Runtime ports for the new verbs
120
+
121
+ - **`ServerStats` port** (`packages/irc-core/src/ports.ts`): async
122
+ `getStats()` returning `ServerStatsSnapshot` (users / invisible /
123
+ opers / unknown / channels / servers / local+global conn counts /
124
+ uptime anchor). Asynchronous because the aggregation crosses every
125
+ connection and channel — it cannot be pre-loaded into a synchronous
126
+ view the way MOTD or a credential table can. Each adapter implements
127
+ it against its authoritative store; `InMemoryStats` is the reference
128
+ impl. The `computeStatsSnapshot` helper holds the shared
129
+ classification logic (oper / invisible / unknown).
130
+ - **`reloadConfig()` on `IrcRuntime`** (`packages/irc-server/src/runtime.ts`):
131
+ request/response port method the `REHASH` route calls to re-fetch
132
+ `ServerConfig` from the adapter's bound source (CF: KV/secret env;
133
+ AWS: Secrets Manager/SSM env; local-cli: the config file/options).
134
+ Modelled as a runtime method rather than a fire-and-forget effect
135
+ because the refreshed value must come back to the actor.
136
+
137
+ ### ⚠️ Migration required
138
+
139
+ - **For adapter authors wiring the new ports.** `ServerStats` and
140
+ `reloadConfig` are optional at the type level (the actor falls back
141
+ to a zero-count snapshot when no `ServerStats` is bound, and
142
+ `REHASH` still emits `382` even if `reloadConfig` rejects — the
143
+ previous config is retained). To surface real counts and live
144
+ reloads, each adapter wires its own backend. No Durable-Object
145
+ persisted-state schema change (`PERSISTED_STATE_VERSION` stays `1`);
146
+ code rollback across this release is safe.
147
+ - No toolchain change. Node ≥ 24 / pnpm 11 (from v0.4.0) still apply.
148
+
149
+ ### Known limitations
150
+
151
+ - Same as v0.4.0's transport limits: the **CF TCP path requires
152
+ Cloudflare Spectrum (Enterprise tier)** plus a stateful Container
153
+ origin; the **AWS TCP path** uses NLB + Lambda streaming and is
154
+ subject to Lambda idle-timeout / stream-duration limits. The
155
+ WebSocket path remains the zero-extra-deps default.
156
+ - **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
157
+ CA/trust-store** on both platforms and is not enabled by default.
158
+ - `serverName` still defaults to the `irc.example.com` placeholder when
159
+ an adapter config omits it (pending).
160
+ - `STATS` ships the `u` + `l` query letters; the full RFC matrix
161
+ (`k`, `K`, `q`, `g`, `x`, `z`, …) is intentionally out of scope for
162
+ v1 — unknown letters get the charybdis-style empty body + `219`.
163
+ - Load testing (10k concurrent connections) and the client
164
+ compatibility sweep (WeeChat / HexChat / IRCCloud / TheLounge)
165
+ remain pending.
166
+ - **Local coverage instrumentation on the Cloudflare-backed packages
167
+ (`@serverless-ircd/cf-adapter`, `@serverless-ircd/cf-worker`) is
168
+ incompatible with the current vitest toolchain.** `@vitest/coverage-v8`
169
+ v4.1.x imports `node:inspector/promises`, which the
170
+ `@cloudflare/vitest-pool-workers` (workerd) test pool cannot resolve.
171
+ The packages' test suites themselves pass (cf-adapter 185/185,
172
+ cf-worker 3/3); only `pnpm coverage` fails to load the coverage
173
+ module under the workerd pool. This is a documented [vitest-pool-workers
174
+ known issue](https://developers.cloudflare.com/workers/testing/vitest-integration/known-issues/#module-resolution)
175
+ and is expected to resolve with an upstream `@vitest/coverage-v8` or
176
+ pool-workers release. No production code is affected; CI behavior
177
+ depends on its installed versions.
178
+
179
+ ### Security
180
+
181
+ - None. No vulnerabilities have been reported or fixed in this release.
182
+ The dropped S2S / obsolete verbs and their reserved numerics carried
183
+ no reachable code path; their removal reduces surface area but fixes
184
+ no defect.
185
+
186
+ ---
187
+
188
+ ## [0.4.0] - 2026-07-31
189
+
190
+ The headline is **dual transport**: both adapters now speak a real
191
+ `irc+tls :6697` (TLS-over-TCP) surface for stock IRC clients in
192
+ production — not just the local CLI — alongside the WebSocket path. This
193
+ lands **SASL `EXTERNAL` via mTLS**, the **remaining standard IRC verbs**
194
+ (WHOWAS, VERSION, TIME, ADMIN, INFO, USERHOST, ISON), a **Cloudflare
195
+ D1-backed SASL account store**, config-driven server identity, and a
196
+ **Node 24 / pnpm 11** toolchain. Phase 7 (TLS hardening & raw TCP) is
197
+ complete.
198
+
199
+ ### Added — Dual transport: WebSocket + `irc+tls :6697`
200
+
201
+ The wss-only transport decision (ADR-005) is superseded by **ADR-009**
202
+ (dual transport). The shared `ConnectionActor` now drives either
203
+ transport through a generalized seam; the parser/reducer/dispatch
204
+ pipeline is unchanged.
205
+
206
+ - **Transport seam** (`@serverless-ircd/irc-server`,
207
+ `@serverless-ircd/irc-core`): the `\r\n` line-framing is extracted into
208
+ a `Transport` abstraction. `ConnectionActor` accepts a
209
+ `WsTextFrameTransport` (one message per WS frame, prior behavior) or a
210
+ `TcpByteStreamTransport` (stateful `\r\n` framing across chunks with
211
+ partial-line buffering). Both produce identical reducer invocations.
212
+ - **Cloudflare TCP+TLS** (`@serverless-ircd/cf-tcp-container`): a new
213
+ workspace app — **Cloudflare Spectrum** terminates TLS at the edge and
214
+ forwards plaintext TCP to a stateful **Container** origin running the
215
+ IRC core. Ships a `Dockerfile`, config loader, deploy pipeline
216
+ (`.github/workflows/deploy-cf-tcp.yml`), and root
217
+ `deploy:cf-tcp:staging` / `deploy:cf-tcp:prod` scripts.
218
+ - **AWS TCP+TLS** (`@serverless-ircd/aws-adapter`, `@serverless-ircd/aws-stack`):
219
+ a **Network Load Balancer** with a `tls` listener terminates TLS and
220
+ invokes a **Lambda streaming function** (`nlb-stream.ts`) as the target.
221
+ Connection identity is derived from NLB flow metadata; bidirectional
222
+ traffic flows over the Lambda response stream, reusing `AwsRuntime`.
223
+ CDK constructs for NLB + target group + ACM cert are added.
224
+ - **Transport-parametrized contract suite** (`@serverless-ircd/irc-test-support`):
225
+ every scenario now runs across a `Transport` dimension
226
+ (in-memory+WS, in-memory+TCP, …), with TCP-specific framing scenarios
227
+ (chunk boundaries, mixed `\r\n`/`\n` line endings, partial sends).
228
+ - **Docs**: `docs/Cloudflare-TCP-Deployment.md` and
229
+ `docs/AWS-TCP-Deployment.md` — end-to-end :6697 deployment guides
230
+ (prerequisites, mTLS trust-store setup, cost/ops caveats).
231
+
232
+ ### Added — SASL `EXTERNAL` via mTLS (`@serverless-ircd/irc-core`, adapters)
233
+
234
+ - **mTLS support**: an `MtlsIdentityProvider` port in `irc-core` feeds
235
+ the verified client-certificate subject into `AccountStore`. Cloudflare
236
+ uses API Shield mTLS (uploaded CA pool); AWS uses API Gateway
237
+ custom-domain mTLS (PEM trust store, subject in the `$connect` event).
238
+ Identity is plumbed via a port — no cloud deps in `irc-core`.
239
+ - **`EXTERNAL` now authenticates** (was a stubbed `904 ERR_SASLFAIL` in
240
+ v0.2.0): `AUTHENTICATE EXTERNAL` against a bound provider succeeds with
241
+ `900 RPL_LOGGEDIN` / `903 RPL_SASLSUCCESS` when the cert maps to an
242
+ account.
243
+ - **mTLS-conditional advertisement**: the `sasl` cap value and
244
+ `908 ERR_SASLMECHS` are now driven by whether an mTLS provider is bound
245
+ — `PLAIN` only by default, `PLAIN,EXTERNAL` when one is. `AUTHENTICATE
246
+ EXTERNAL` without mTLS returns `908` (mechanism not available) instead
247
+ of the misleading `904`. Operators enable EXTERNAL by configuring edge
248
+ mTLS — no code change required.
249
+
250
+ ### Added — Remaining IRC query verbs (`@serverless-ircd/irc-core`)
251
+
252
+ - **`WHOWAS`**: a nick-history store (port + in-memory reference impl)
253
+ records `(nick, connId, username, hostname, signoffTime)` on QUIT /
254
+ NICK-change and purges past a TTL. `WHOWAS <nick>{,<nick>} [<count>]`
255
+ queries it — `314 RPL_WHOWASUSER` per match, `369 RPL_ENDOFWHOWAS`
256
+ terminator, `406 ERR_WASNOSUCHNICK` when no match.
257
+ - **`VERSION`** (`351`), **`TIME`** (`391`, uses the injected `Clock`),
258
+ **`ADMIN`** (`256`/`257`/`258`), **`INFO`** (`371`/`374`),
259
+ **`USERHOST`** (`302`), **`ISON`** (`303`) — each a reducer + a route
260
+ case with spec-correct numerics. Completes the standard query-verb set.
261
+
262
+ ### Added — Server identity from config (`@serverless-ircd/irc-core`)
263
+
264
+ - The `002` / `003` / `004` registration replies now reflect
265
+ `ServerConfig.serverVersion` / `createdAt` — the hardcoded
266
+ `'0.1.0'` / `'Phase 1'` placeholders are gone. A build step
267
+ (`scripts/generate-build-info.mjs`) bakes the package version and a
268
+ build timestamp into `RPL_CREATED` (`003`) when the config omits them.
269
+
270
+ ### Added — Cloudflare D1-backed SASL account store (`@serverless-ircd/cf-adapter`, `@serverless-ircd/irc-core`)
271
+
272
+ - Brings the CF adapter to parity with the AWS `DynamoAccountStore`. A
273
+ persistent `AccountStore` pre-loads scrypt-hashed rows from a new
274
+ **D1** database (`ACCOUNTS_DB` binding) at `ConnectionDO`
275
+ construction.
276
+ - **Shared scrypt hashing**: `hashAccountCredential` /
277
+ `verifyHashedPassword` are extracted to `irc-core` so both adapters use
278
+ one implementation (never plaintext).
279
+ - **Table-then-env precedence**: when the D1 table has rows it is
280
+ authoritative; when empty or unreachable the adapter falls back to the
281
+ `SASL_ACCOUNTS` env-var seed, preserving current behavior.
282
+ - **`tools/seed-cf-accounts.ts`**: provisioning CLI that scrypt-hashes
283
+ `username:password` pairs into the D1 `accounts` table (re-run
284
+ overwrites the prior row).
285
+
286
+ ### Added — AWS max-clients admission (`@serverless-ircd/aws-adapter`)
287
+
288
+ - `$connect` now enforces `serverConfig.maxClients` (was a reserved
289
+ no-op — `void params.serverConfig;`). Connections over the cap are
290
+ rejected at accept time, matching the Cloudflare admission gate.
291
+
292
+ ### Changed
293
+
294
+ - **Toolchain**: bumped from Node ≥ 20 / pnpm 9 to **Node ≥ 24 / pnpm
295
+ 11** (aligns the Lambda and Cloudflare Workers runtimes). Re-run
296
+ `corepack enable` to pick up the pinned pnpm.
297
+ - `@serverless-ircd/irc-core`: the request/response lookup `Effect`
298
+ variants (`LookupNick`, `GetConnectionInfo`, `GetChannelSnapshot`) are
299
+ removed from `effects.ts` and `dispatch` — they were dead scaffolding
300
+ with no producer or consumer.
301
+ - `@serverless-ircd/cf-adapter`: the dead channel-registration loop in
302
+ `ConnectionDO.webSocketMessage` and the reserved outbound-batching
303
+ field are removed (the latter was a never-wired optimization).
304
+
305
+ ### ⚠️ Migration required
306
+
307
+ - **Cloudflare: new D1 binding for SASL accounts.** To persist SASL
308
+ accounts, create the D1 database and add the `ACCOUNTS_DB` binding
309
+ (default + staging) to `wrangler.toml`:
310
+ ```bash
311
+ wrangler d1 create serverless-ircd-accounts
312
+ wrangler d1 execute serverless-ircd-accounts --command \
313
+ "CREATE TABLE IF NOT EXISTS accounts (account TEXT PRIMARY KEY, algorithm TEXT NOT NULL, salt TEXT NOT NULL, hash TEXT NOT NULL)"
314
+ ```
315
+ **No action is required to keep working** — when the D1 table is empty
316
+ or unreachable the adapter falls back to the `SASL_ACCOUNTS` env-var
317
+ seed. There is **no Durable-Object persisted-state schema change**
318
+ (`PERSISTED_STATE_VERSION` stays `1`); code rollback across this
319
+ release is safe.
320
+ - **Local toolchain**: Node ≥ 24 / pnpm 11 is now required. Re-run
321
+ `corepack enable`.
322
+
323
+ ### Known limitations
324
+
325
+ - The **CF TCP path requires Cloudflare Spectrum (Enterprise tier)** plus
326
+ a stateful Container origin; the **AWS TCP path** uses NLB + Lambda
327
+ streaming and is subject to Lambda idle-timeout / stream-duration
328
+ limits. The WebSocket path remains the zero-extra-deps default.
329
+ - **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
330
+ CA/trust-store** on both platforms and is not enabled by default.
331
+ - Oper-gated verbs (`KILL`, `REHASH`, `CONNECT`, `SQUIT`, `TRACE`,
332
+ `WALLOPS`, `SETNAME`, …) still return `421 ERR_UNKNOWNCOMMAND`.
333
+ - `serverName` still defaults to the `irc.example.com` placeholder when
334
+ an adapter config omits it (pending).
335
+
336
+ ### Security
337
+
338
+ - SASL `EXTERNAL` no longer returns a misleading `904 ERR_SASLFAIL` when
339
+ mTLS isn't configured — it returns `908 ERR_SASLMECHS` and the
340
+ mechanism is not advertised until a trust store is bound. SASL
341
+ credentials remain scrypt-hashed only (never plaintext), now via a
342
+ single shared implementation across both adapters.
343
+
344
+ ---
345
+
13
346
  ## [0.3.0] - 2026-07-25
14
347
 
15
348
  ### Added — Operator authentication (`@serverless-ircd/irc-core`)
@@ -378,31 +711,29 @@ manual per `docs/release.md` §8.3.
378
711
 
379
712
  ### Known limitations
380
713
 
381
- - **No AWS adapter.** Tickets TICKET-039 through TICKET-044 (CDK
382
- stack, APIGW WebSocket handlers, DynamoDB transactions, EventBridge
383
- timers, deploy pipeline, deployment doc) are pending. Cloudflare is
384
- the only deployable platform in 0.1.0.
714
+ - **No AWS adapter.** The CDK stack, APIGW WebSocket handlers, DynamoDB
715
+ transactions, EventBridge timers, deploy pipeline, and deployment doc
716
+ are pending. Cloudflare is the only deployable platform in 0.1.0.
385
717
  - **No SASL.** `CAP sasl` is advertised in the capability list but
386
718
  `AUTHENTICATE` is not wired into the dispatcher. Plain-text password
387
- authentication via `PASS` is also not enforced (TICKET-046).
388
- - **No flood control.** A token-bucket wrapper (TICKET-028) is
389
- pending. Misbehaving clients can send unbounded commands.
719
+ authentication via `PASS` is also not enforced.
720
+ - **No flood control.** A token-bucket wrapper is pending. Misbehaving
721
+ clients can send unbounded commands.
390
722
  - **No server-password enforcement, rate limits, cloaking, or oper
391
- credentials** (TICKET-046).
723
+ credentials.**
392
724
  - **No config loader.** Server name, network name, MOTD, channel
393
- prefixes, max clients, oper creds are hardcoded defaults
394
- (TICKET-047). The `MOTD.txt` file at the repo root is informational
395
- only; the live MOTD is compiled into the Worker from
396
- `[vars].MOTD_LINES`.
725
+ prefixes, max clients, oper creds are hardcoded defaults. The
726
+ `MOTD.txt` file at the repo root is informational only; the live MOTD
727
+ is compiled into the Worker from `[vars].MOTD_LINES`.
397
728
  - **No CI coverage gate.** Coverage is computed and uploaded as an
398
- artifact, but no threshold is enforced (TICKET-048). Inspect
729
+ artifact, but no threshold is enforced. Inspect
399
730
  `packages/*/coverage/` before deploying.
400
731
  - **No TLS / raw TCP transport.** v1 is WebSocket text frames only.
401
- TLS-hardened and `irc+tls` transports are 0.x work (TICKET-052
402
- through TICKET-058). Stock TCP IRC clients reach the server through
403
- the local `tcp-ws-forwarder` bridge.
404
- - **No `chathistory` playback.** TICKET-063 is pending; clients that
405
- request backlog on JOIN will see an empty batch or no response.
732
+ TLS-hardened and `irc+tls` transports are 0.x work. Stock TCP IRC
733
+ clients reach the server through the local `tcp-ws-forwarder` bridge.
734
+ - **No `chathistory` playback.** Chat-history playback is pending;
735
+ clients that request backlog on JOIN will see an empty batch or no
736
+ response.
406
737
 
407
738
  ### Security
408
739