serverless-ircd 0.4.0 → 0.6.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 (243) hide show
  1. package/.github/workflows/ci.yml +34 -2
  2. package/.github/workflows/deploy-aws.yml +1 -2
  3. package/.github/workflows/deploy-cf-tcp.yml +1 -5
  4. package/.github/workflows/deploy-cf.yml +1 -2
  5. package/CHANGELOG.md +435 -0
  6. package/README.md +206 -27
  7. package/apps/aws-stack/README.md +37 -3
  8. package/apps/aws-stack/package.json +1 -1
  9. package/apps/aws-stack/src/aws-stack.ts +106 -13
  10. package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
  11. package/apps/aws-stack/tests/stack.test.ts +49 -3
  12. package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
  13. package/apps/aws-stack/tests/vitest.setup.ts +18 -0
  14. package/apps/aws-stack/vitest.config.ts +3 -0
  15. package/apps/cf-tcp-container/package.json +1 -1
  16. package/apps/cf-tcp-container/src/config-loader.ts +6 -7
  17. package/apps/cf-tcp-container/src/container-server.ts +3 -0
  18. package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
  19. package/apps/cf-tcp-container/wrangler.toml +1 -1
  20. package/apps/cf-worker/package.json +2 -1
  21. package/apps/cf-worker/vitest.config.ts +1 -1
  22. package/apps/cf-worker/wrangler.toml +5 -3
  23. package/apps/local-cli/package.json +1 -1
  24. package/apps/local-cli/src/config-loader.ts +6 -6
  25. package/apps/local-cli/src/main.ts +20 -0
  26. package/apps/local-cli/src/server.ts +138 -28
  27. package/apps/local-cli/tests/e2e.test.ts +113 -29
  28. package/apps/local-cli/tests/tcp.test.ts +71 -19
  29. package/apps/local-cli/tests/ws-subprotocol.test.ts +262 -0
  30. package/biome.json +1 -0
  31. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  32. package/docs/ADR-002-location-of-authority.md +82 -0
  33. package/docs/ADR-003-durable-object-sharding.md +110 -0
  34. package/docs/ADR-004-dynamodb-schema.md +96 -0
  35. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  36. package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
  37. package/docs/ADR-007-deterministic-ports.md +82 -0
  38. package/docs/ADR-008-monorepo-tooling.md +60 -0
  39. package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
  40. package/docs/ADR-Index.md +15 -0
  41. package/docs/AWS-Adapter-Architecture.md +496 -0
  42. package/docs/AWS-Deployment.md +1275 -0
  43. package/docs/AWS-TCP-Deployment.md +410 -0
  44. package/docs/Cloudflare-Deployment-Guide.md +719 -0
  45. package/docs/Cloudflare-TCP-Deployment.md +510 -0
  46. package/docs/Home.md +15 -0
  47. package/docs/Observability.md +87 -0
  48. package/docs/PlanExtensions.md +812 -0
  49. package/docs/PlanIRCv3Websocket.md +503 -0
  50. package/docs/PlanWebClient.md +451 -0
  51. package/docs/Release-Process.md +443 -0
  52. package/package.json +2 -2
  53. package/packages/aws-adapter/package.json +1 -1
  54. package/packages/aws-adapter/src/aws-runtime.ts +69 -0
  55. package/packages/aws-adapter/src/handlers/connect.ts +36 -5
  56. package/packages/aws-adapter/src/handlers/default.ts +66 -5
  57. package/packages/aws-adapter/src/handlers/index.ts +41 -2
  58. package/packages/aws-adapter/src/handlers/nlb-stream.ts +18 -0
  59. package/packages/aws-adapter/src/index.ts +2 -0
  60. package/packages/aws-adapter/src/serialize.ts +40 -2
  61. package/packages/aws-adapter/src/stats.ts +80 -0
  62. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
  63. package/packages/aws-adapter/tests/account-store.test.ts +4 -7
  64. package/packages/aws-adapter/tests/aws-harness.ts +5 -2
  65. package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
  66. package/packages/aws-adapter/tests/aws-runtime.test.ts +140 -0
  67. package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
  68. package/packages/aws-adapter/tests/connect.test.ts +100 -4
  69. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
  70. package/packages/aws-adapter/tests/global-setup.ts +55 -30
  71. package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
  72. package/packages/aws-adapter/tests/handlers.test.ts +238 -4
  73. package/packages/aws-adapter/tests/message-store.test.ts +6 -5
  74. package/packages/aws-adapter/tests/nlb-stream.test.ts +62 -8
  75. package/packages/aws-adapter/tests/stats.test.ts +317 -0
  76. package/packages/cf-adapter/package.json +6 -1
  77. package/packages/cf-adapter/src/cf-runtime.ts +66 -1
  78. package/packages/cf-adapter/src/channel-do.ts +2 -2
  79. package/packages/cf-adapter/src/connection-do.ts +185 -54
  80. package/packages/cf-adapter/src/env.ts +25 -6
  81. package/packages/cf-adapter/src/index.ts +2 -0
  82. package/packages/cf-adapter/src/registry-do.ts +22 -3
  83. package/packages/cf-adapter/src/serialize.ts +25 -4
  84. package/packages/cf-adapter/src/sharding.ts +1 -2
  85. package/packages/cf-adapter/src/stats.ts +65 -0
  86. package/packages/cf-adapter/tests/cf-harness.ts +1 -1
  87. package/packages/cf-adapter/tests/cf-integration.test.ts +4 -4
  88. package/packages/cf-adapter/tests/cf-runtime.test.ts +307 -2
  89. package/packages/cf-adapter/tests/channel-do.test.ts +119 -2
  90. package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
  91. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +2 -2
  92. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +2 -2
  93. package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
  94. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
  95. package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
  96. package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
  97. package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
  98. package/packages/cf-adapter/tests/registry-do.test.ts +108 -4
  99. package/packages/cf-adapter/tests/serialize.test.ts +308 -0
  100. package/packages/cf-adapter/tests/sharding.test.ts +1 -1
  101. package/packages/cf-adapter/tests/stats.test.ts +120 -0
  102. package/packages/cf-adapter/tests/worker/main.ts +7 -7
  103. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
  104. package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
  105. package/packages/cf-adapter/vitest.config.ts +1 -1
  106. package/packages/cf-adapter/wrangler.test.toml +7 -0
  107. package/packages/in-memory-runtime/package.json +1 -1
  108. package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
  109. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
  110. package/packages/irc-core/package.json +1 -1
  111. package/packages/irc-core/src/admission.ts +16 -15
  112. package/packages/irc-core/src/caps/capabilities.ts +38 -4
  113. package/packages/irc-core/src/caps/index.ts +1 -0
  114. package/packages/irc-core/src/caps/sts.ts +84 -0
  115. package/packages/irc-core/src/commands/account-notify.ts +53 -0
  116. package/packages/irc-core/src/commands/away.ts +9 -3
  117. package/packages/irc-core/src/commands/cap.ts +23 -5
  118. package/packages/irc-core/src/commands/chathistory.ts +40 -11
  119. package/packages/irc-core/src/commands/index.ts +42 -0
  120. package/packages/irc-core/src/commands/invite.ts +2 -4
  121. package/packages/irc-core/src/commands/isupport.ts +59 -2
  122. package/packages/irc-core/src/commands/kick.ts +2 -4
  123. package/packages/irc-core/src/commands/kill.ts +127 -0
  124. package/packages/irc-core/src/commands/list.ts +1 -1
  125. package/packages/irc-core/src/commands/lusers.ts +204 -0
  126. package/packages/irc-core/src/commands/mode.ts +12 -9
  127. package/packages/irc-core/src/commands/monitor.ts +327 -0
  128. package/packages/irc-core/src/commands/multiline.ts +256 -0
  129. package/packages/irc-core/src/commands/names.ts +3 -5
  130. package/packages/irc-core/src/commands/part.ts +2 -4
  131. package/packages/irc-core/src/commands/pre-away.ts +112 -0
  132. package/packages/irc-core/src/commands/privmsg.ts +19 -12
  133. package/packages/irc-core/src/commands/read-marker.ts +108 -0
  134. package/packages/irc-core/src/commands/registration.ts +8 -0
  135. package/packages/irc-core/src/commands/rehash.ts +119 -0
  136. package/packages/irc-core/src/commands/sasl.ts +24 -1
  137. package/packages/irc-core/src/commands/setname.ts +109 -0
  138. package/packages/irc-core/src/commands/stats.ts +152 -0
  139. package/packages/irc-core/src/commands/tagmsg.ts +71 -1
  140. package/packages/irc-core/src/commands/topic.ts +2 -4
  141. package/packages/irc-core/src/commands/trace.ts +137 -0
  142. package/packages/irc-core/src/commands/wallops.ts +118 -0
  143. package/packages/irc-core/src/commands/whois.ts +5 -0
  144. package/packages/irc-core/src/config.ts +72 -10
  145. package/packages/irc-core/src/effects.ts +41 -1
  146. package/packages/irc-core/src/index.ts +2 -0
  147. package/packages/irc-core/src/ports.ts +568 -0
  148. package/packages/irc-core/src/protocol/index.ts +14 -0
  149. package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
  150. package/packages/irc-core/src/protocol/numerics.ts +57 -11
  151. package/packages/irc-core/src/protocol/outbound.ts +36 -4
  152. package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
  153. package/packages/irc-core/src/state/connection.ts +32 -1
  154. package/packages/irc-core/src/types.ts +120 -1
  155. package/packages/irc-core/src/ws-framing.ts +132 -0
  156. package/packages/irc-core/src/ws-subprotocol.ts +66 -0
  157. package/packages/irc-core/stryker.commands.conf.json +1 -2
  158. package/packages/irc-core/tests/admission.test.ts +18 -0
  159. package/packages/irc-core/tests/away-store.test.ts +73 -0
  160. package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
  161. package/packages/irc-core/tests/caps/sts.test.ts +123 -0
  162. package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
  163. package/packages/irc-core/tests/commands/away.test.ts +85 -2
  164. package/packages/irc-core/tests/commands/cap.test.ts +165 -0
  165. package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
  166. package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
  167. package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
  168. package/packages/irc-core/tests/commands/kill.test.ts +243 -0
  169. package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
  170. package/packages/irc-core/tests/commands/mode.test.ts +129 -0
  171. package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
  172. package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
  173. package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
  174. package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
  175. package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
  176. package/packages/irc-core/tests/commands/registration.test.ts +64 -0
  177. package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
  178. package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
  179. package/packages/irc-core/tests/commands/setname.test.ts +225 -0
  180. package/packages/irc-core/tests/commands/stats.test.ts +294 -0
  181. package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
  182. package/packages/irc-core/tests/commands/trace.test.ts +282 -0
  183. package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
  184. package/packages/irc-core/tests/commands/whois.test.ts +34 -0
  185. package/packages/irc-core/tests/config.test.ts +103 -13
  186. package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
  187. package/packages/irc-core/tests/effects.test.ts +14 -0
  188. package/packages/irc-core/tests/labeled-response.test.ts +181 -0
  189. package/packages/irc-core/tests/message-tags.test.ts +114 -0
  190. package/packages/irc-core/tests/monitor-store.test.ts +200 -0
  191. package/packages/irc-core/tests/numerics.test.ts +90 -0
  192. package/packages/irc-core/tests/outbound.test.ts +51 -0
  193. package/packages/irc-core/tests/ports.test.ts +22 -0
  194. package/packages/irc-core/tests/raw-modules.d.ts +11 -0
  195. package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
  196. package/packages/irc-core/tests/standard-replies.test.ts +412 -0
  197. package/packages/irc-core/tests/stats-store.test.ts +222 -0
  198. package/packages/irc-core/tests/types.test.ts +27 -0
  199. package/packages/irc-core/tests/ws-framing.test.ts +213 -0
  200. package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
  201. package/packages/irc-server/package.json +1 -1
  202. package/packages/irc-server/src/actor.ts +699 -19
  203. package/packages/irc-server/src/dispatch.ts +109 -16
  204. package/packages/irc-server/src/routing.ts +3 -0
  205. package/packages/irc-server/src/runtime.ts +31 -0
  206. package/packages/irc-server/src/transport.ts +10 -7
  207. package/packages/irc-server/tests/actor.test.ts +2523 -42
  208. package/packages/irc-server/tests/dispatch.test.ts +300 -2
  209. package/packages/irc-server/tests/raw-modules.d.ts +11 -0
  210. package/packages/irc-server/tests/routing.test.ts +1 -0
  211. package/packages/irc-server/tests/runtime.test.ts +7 -0
  212. package/packages/irc-test-support/package.json +1 -1
  213. package/packages/irc-test-support/src/index.ts +6 -0
  214. package/packages/irc-test-support/src/scenarios.ts +9 -1
  215. package/packages/irc-test-support/src/test-config.ts +54 -0
  216. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
  217. package/packages/irc-test-support/tests/test-config.test.ts +51 -0
  218. package/pnpm-workspace.yaml +1 -0
  219. package/tools/ci-hardening/package.json +1 -1
  220. package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
  221. package/tools/load-test/package.json +33 -0
  222. package/tools/load-test/src/client.ts +351 -0
  223. package/tools/load-test/src/config.ts +313 -0
  224. package/tools/load-test/src/harness.ts +116 -0
  225. package/tools/load-test/src/main.ts +120 -0
  226. package/tools/load-test/src/metrics.ts +168 -0
  227. package/tools/load-test/src/report.ts +106 -0
  228. package/tools/load-test/tests/client.test.ts +212 -0
  229. package/tools/load-test/tests/config.test.ts +152 -0
  230. package/tools/load-test/tests/framing.test.ts +37 -0
  231. package/tools/load-test/tests/harness.test.ts +165 -0
  232. package/tools/load-test/tests/metrics.test.ts +174 -0
  233. package/tools/load-test/tests/report.test.ts +161 -0
  234. package/tools/load-test/tests/smoke.test.ts +67 -0
  235. package/tools/load-test/tsconfig.build.json +12 -0
  236. package/tools/load-test/tsconfig.test.json +10 -0
  237. package/tools/load-test/vitest.config.ts +29 -0
  238. package/tools/package.json +6 -1
  239. package/tools/seed-cf-accounts.ts +4 -1
  240. package/tools/tcp-ws-forwarder/package.json +1 -1
  241. package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
  242. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
  243. package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
@@ -0,0 +1,510 @@
1
+ # Cloudflare TCP+TLS Deployment Guide (`irc+tls` :6697)
2
+
3
+ End-to-end guide for deploying the **raw TCP+TLS** transport of
4
+ ServerlessIRCd to Cloudflare (Phase 7 of `PLAN.md`, TICKET-055). This is
5
+ the RFC 1459/2812-compliant path that older clients, bots, and bridges
6
+ use. The default **wss** transport is documented separately in
7
+ [`Cloudflare-Deployment-Guide.md`](./Cloudflare-Deployment-Guide.md);
8
+ the two are independent and a deployment can enable either or both.
9
+
10
+ > **Read first:** [`ADR-009`](./ADR-009-dual-transport-wss-and-irc-tls.md)
11
+ > for *why* a second transport exists and the per-platform mapping.
12
+ > This doc covers the *how*.
13
+
14
+ **Acceptance criterion (TICKET-058):** a new contributor can deploy
15
+ their own TCP+TLS staging instance following only this doc.
16
+
17
+ Cross-reference: `apps/cf-tcp-container/` (origin),
18
+ `apps/cf-tcp-container/wrangler.toml` (container config),
19
+ `apps/cf-tcp-container/terraform/spectrum.tf` (Spectrum TLS config),
20
+ `.github/workflows/deploy-cf-tcp.yml` (CI), TICKET-054 (mTLS), ADR-006
21
+ (SASL mechanism scope).
22
+
23
+ ---
24
+
25
+ ## 1. What gets deployed
26
+
27
+ The wss path is pure-serverless (Worker + Durable Objects). The TCP path
28
+ **cannot** be — Cloudflare Workers cannot accept raw TCP. So the TCP+TLS
29
+ path adds two stateful pieces in front of the shared IRC core:
30
+
31
+ ```
32
+ IRC client (irc+tls :6697)
33
+
34
+ │ TLS handshake (client ⟷ Cloudflare)
35
+
36
+ ┌──────────────────────────────────────────────────────────────┐
37
+ │ Cloudflare Spectrum (Enterprise) │
38
+ │ • terminates TLS on :6697 │
39
+ │ • forwards plaintext TCP to the origin │
40
+ │ • PROXY protocol v1 carries the real client IP │
41
+ └──────────────────────────┬───────────────────────────────────┘
42
+ │ plaintext TCP (:6667)
43
+
44
+ ┌──────────────────────────────────────────────────────────────┐
45
+ │ Cloudflare Container (apps/cf-tcp-container) │
46
+ │ • node:24 process, EXPOSE 6667 │
47
+ │ • ConnectionActor + InMemoryRuntime + TcpByteStream- │
48
+ │ Transport (the shared transport seam, TICKET-053) │
49
+ │ • persistence snapshot at /data/state.json │
50
+ │ (channel topology survives restart; live connections │
51
+ │ do not — clients reconnect) │
52
+ └──────────────────────────────────────────────────────────────┘
53
+ ```
54
+
55
+ The **parser → reducer → dispatch pipeline is shared verbatim** with the
56
+ wss path. Only the line-framing transport differs: a `TcpByteStreamTransport`
57
+ reassembles `\r\n`-terminated IRC lines across arbitrary TCP chunks
58
+ (partial-line buffering), whereas the wss Worker uses one-message-per-frame.
59
+
60
+ Two artifacts are deployed and configured:
61
+
62
+ | Artifact | Tool | What it does |
63
+ |---------------------------------------|----------------|------------------------------------------------|
64
+ | `sirc-tcp-origin` Cloudflare Container| `wrangler` | Builds + runs the IRC core container image. |
65
+ | `cloudflare_spectrum_application` | `terraform` | TLS termination on :6697 + origin forwarding. |
66
+
67
+ ---
68
+
69
+ ## 2. Prerequisites
70
+
71
+ | Requirement | Detail |
72
+ |--------------------------|---------------------------------------------------------------------|
73
+ | Node.js | ≥ 24 (`engines.node` in root `package.json`). |
74
+ | pnpm | 11.x (`packageManager: pnpm@11.17.0`). |
75
+ | `wrangler` CLI | v4.112+ (comes from `apps/cf-tcp-container/devDependencies`). |
76
+ | `terraform` CLI | ≥ 1.5 (for the Spectrum step). Install separately. |
77
+ | **Cloudflare Enterprise**| **Spectrum is an Enterprise-tier feature.** Required for the TCP path. |
78
+ | Containers beta access | Cloudflare Containers is in beta; request access in the dashboard. |
79
+ | A Cloudflare zone | The public hostname (e.g. `irc.example.com`) must live in a zone you control. |
80
+ | Git checkout | Clean working tree on `main` for production deploys. |
81
+
82
+ > The wss path runs on the Workers Free/Paid plan. The TCP path does
83
+ > **not** — Spectrum mandates Enterprise. If you do not have Enterprise,
84
+ > deploy the wss path instead (see `Cloudflare-Deployment-Guide.md`).
85
+
86
+ Confirm the local environment:
87
+
88
+ ```bash
89
+ node --version # v24.x or newer
90
+ pnpm --version # 11.x
91
+ terraform version # >= 1.5
92
+ pnpm install # from repo root
93
+ pnpm build # builds irc-core, irc-server, in-memory-runtime, cf-tcp-container
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 3. First-time setup
99
+
100
+ Done once per Cloudflare account + zone.
101
+
102
+ ### 3.1 API token
103
+
104
+ Same token shape as the wss path works, but it additionally needs
105
+ Spectrum edit rights. Create a token at
106
+ <https://dash.cloudflare.com/profile/api-tokens> with:
107
+
108
+ - `Account · Workers Scripts · Edit` (Containers deploy)
109
+ - `Zone · Spectrum · Edit` (terraform `cloudflare_spectrum_application`)
110
+ - `Zone · DNS · Edit` (Spectrum creates a CNAME)
111
+
112
+ Restrict to the target zone. Set as `CLOUDFLARE_API_TOKEN` locally and as
113
+ a GitHub Actions secret for CI.
114
+
115
+ ### 3.2 Collect the IDs the deploy needs
116
+
117
+ | Secret / var | Where to find it |
118
+ |--------------------------|---------------------------------------------------------|
119
+ | `CLOUDFLARE_ACCOUNT_ID` | Dashboard sidebar (any zone → Overview, lower right). |
120
+ | `CF_ZONE_ID` | Dashboard sidebar of the target zone → Overview. |
121
+ | `CF_TCP_ORIGIN_ADDRESS` | The Container's origin hostname (printed after first `wrangler deploy`, see §5.1). |
122
+
123
+ ### 3.3 Authenticate the CLI
124
+
125
+ ```bash
126
+ npx wrangler login # interactive
127
+ # or non-interactive:
128
+ export CLOUDFLARE_API_TOKEN=...
129
+ export CLOUDFLARE_ACCOUNT_ID=...
130
+ ```
131
+
132
+ ---
133
+
134
+ ## 4. Local development
135
+
136
+ Run the container origin locally without Spectrum — connect a plaintext
137
+ TCP IRC client directly to :6667. This exercises the exact
138
+ `ConnectionActor` + `TcpByteStreamTransport` pipeline.
139
+
140
+ ```bash
141
+ # Build the workspace + the container package.
142
+ pnpm build
143
+
144
+ # Run the origin on plaintext :6667 (default TCP_PORT).
145
+ pnpm --filter @serverless-ircd/cf-tcp-container start \
146
+ -- --env-file <(printf 'SERVER_NAME=irc.local\nTCP_PORT=6667\n')
147
+ ```
148
+
149
+ Connect a plaintext client (no TLS) to `localhost:6667`:
150
+
151
+ ```
152
+ /eval /connect localhost 6667 # WeeChat, no SSL
153
+ NICK alice
154
+ USER alice 0 * :Alice
155
+ JOIN #test
156
+ PRIVMSG #test :hello over plaintext TCP
157
+ ```
158
+
159
+ For a TLS-terminated local loop, put `stunnel` (or `openssl s_server`)
160
+ in front of :6667 and connect on :6697 — mirroring the Spectrum topology.
161
+
162
+ The unit + the TLS e2e suites live in `apps/cf-tcp-container/tests/`
163
+ (including `tls-e2e.test.ts`, which runs a real TLS handshake against the
164
+ origin). Run them with:
165
+
166
+ ```bash
167
+ pnpm --filter @serverless-ircd/cf-tcp-container test
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 5. Deploy staging
173
+
174
+ Staging is what CI deploys on every push that touches
175
+ `apps/cf-tcp-container/**`. You can also deploy it manually.
176
+
177
+ ### 5.1 Deploy the container origin
178
+
179
+ ```bash
180
+ # From the repo root:
181
+ pnpm deploy:cf-tcp:staging
182
+ # Equivalent to:
183
+ # pnpm build && pnpm --filter @serverless-ircd/cf-tcp-container deploy:staging
184
+ # → wrangler deploy --env staging (inside apps/cf-tcp-container)
185
+ ```
186
+
187
+ This builds the container image from `apps/cf-tcp-container/Dockerfile`
188
+ and deploys the `sirc-tcp-origin-staging` Container. The first deploy
189
+ prints the **origin address** — copy it into `CF_TCP_ORIGIN_ADDRESS`
190
+ (used by the Spectrum step next).
191
+
192
+ ### 5.2 Apply the Spectrum TLS config
193
+
194
+ ```bash
195
+ cd apps/cf-tcp-container/terraform
196
+ terraform init
197
+ terraform apply \
198
+ -var zone_id=$CF_ZONE_ID \
199
+ -var origin_address=$CF_TCP_ORIGIN_ADDRESS \
200
+ -var hostname=irc-staging.example.com
201
+ ```
202
+
203
+ This creates the `cloudflare_spectrum_application.irc_tls` resource:
204
+ TLS on :6697 → plaintext TCP to the container origin on :6667.
205
+
206
+ ### 5.3 Smoke test over TLS
207
+
208
+ ```bash
209
+ # Minimal smoke: register + quit over the real TLS endpoint.
210
+ printf 'NICK smoke\r\nUSER smoke 0 * :Smoke\r\nQUIT :smoke\r\n' | \
211
+ timeout 10 openssl s_client -connect irc-staging.example.com:6697 -quiet | \
212
+ grep -q '001' && echo "SMOKE PASS" || echo "SMOKE FAIL"
213
+ ```
214
+
215
+ CI does the same — see `.github/workflows/deploy-cf-tcp.yml` step
216
+ "Smoke e2e (irc+tls)", gated on the `CF_TCP_SMOKE_HOST` repo variable.
217
+
218
+ ### 5.4 Connect a real IRC client
219
+
220
+ ```
221
+ /server add ircd irc-staging.example.com/6697
222
+ /set irc.server.ircd.ssl on
223
+ /connect ircd
224
+ /join #test
225
+ ```
226
+
227
+ ---
228
+
229
+ ## 6. Deploy production
230
+
231
+ Production is the default `wrangler.toml` environment (no `--env` flag).
232
+
233
+ ```bash
234
+ # 1. Edit wrangler.toml [vars] for production identity.
235
+ # 2. Deploy the container:
236
+ pnpm deploy:cf-tcp:prod
237
+ # 3. Apply Spectrum config for the production hostname:
238
+ cd apps/cf-tcp-container/terraform
239
+ terraform apply -var zone_id=$CF_ZONE_ID \
240
+ -var origin_address=$CF_TCP_ORIGIN_ADDRESS \
241
+ -var hostname=irc.example.com
242
+ ```
243
+
244
+ There is **no CI auto-deploy to production** — production deploys are
245
+ manual. Staging deploys on every push to `main` that touches the
246
+ container path.
247
+
248
+ ---
249
+
250
+ ## 7. Configuration reference
251
+
252
+ ### 7.1 Container — `apps/cf-tcp-container/wrangler.toml`
253
+
254
+ Top-level `[vars]` map directly to the container config loader
255
+ (`apps/cf-tcp-container/src/config-loader.ts`):
256
+
257
+ | Var | Purpose | Default |
258
+ |----------------------|------------------------------------------------------|--------------------------|
259
+ | `SERVER_NAME` | Server name in `001`/`005` numerics. **Required** — the container config loader fails fast when unset. | _No default; must be set._ |
260
+ | `NETWORK_NAME` | Network label in `005 NETWORK=…`. | `ExampleNet` |
261
+ | `SERVER_VERSION` | Version string in `002`. | (from package version) |
262
+ | `CREATED_AT` | Creation text/time in `003`. | build/deploy timestamp |
263
+ | `MOTD_LINES` | Message-of-the-day, `\n`-delimited. | welcome banner |
264
+ | `TCP_PORT` | Plaintext port the container listens on. | `6667` |
265
+ | `TCP_HOST` | Bind address (Spectrum forwards from the edge). | `0.0.0.0` |
266
+ | `PERSISTENCE_PATH` | Snapshot file path; enables channel-state survival. | `/data/state.json` |
267
+ | `SNAPSHOT_INTERVAL_MS`| Auto-snapshot cadence (ms). | `60000` |
268
+ | `OPER_USER` / `OPER_PASSWORD` | Oper credentials for `OPER`. | — |
269
+ | `SASL_ACCOUNTS` | `\n`-delimited `user:password` lines for SASL PLAIN. | — |
270
+ | `MAX_CHANNELS_PER_USER`, `MAX_TARGETS_PER_COMMAND`, `NICK_LEN`, `CHANNEL_LEN`, `TOPIC_LEN`, `MAX_LIST_ENTRIES`, `QUIT_MESSAGE` | Standard `ServerConfig` knobs (mirror the wss path). | shared defaults |
271
+
272
+ The container block itself:
273
+
274
+ ```toml
275
+ [[containers]]
276
+ class_name = "IrcTcpOrigin"
277
+ image = "./Dockerfile"
278
+ instance_type = "standard-1"
279
+ max_instances = 3
280
+ sleep_after = "2h"
281
+ ```
282
+
283
+ - `max_instances` caps horizontal scale; the `InMemoryRuntime` is
284
+ per-process, so connections on different instances do not share state.
285
+ Tune `max_instances` to your expected concurrency (one origin handles
286
+ thousands of idle connections thanks to the async TCP server).
287
+ - `sleep_after = "2h"` lets an idle origin scale to zero; the first new
288
+ connection cold-starts it. Persistence (§8) means channel topology is
289
+ not lost across sleep.
290
+
291
+ ### 7.2 Spectrum — `apps/cf-tcp-container/terraform/spectrum.tf`
292
+
293
+ | Variable | Purpose | Default |
294
+ |-------------------|---------------------------------------------------|---------------------|
295
+ | `zone_id` | Cloudflare zone for the hostname. | *(required)* |
296
+ | `origin_address` | Hostname/IP of the container origin. | *(required)* |
297
+ | `origin_port` | Port the origin listens on (plaintext). | `6667` |
298
+ | `hostname` | Public hostname clients connect to. | `irc.example.com` |
299
+ | `spectrum_port` | Public port Spectrum listens on. | `6697` |
300
+ | `tls_mode` | `flexible` (TLS→CF, plain→origin) / `full` / `strict`. | `flexible` |
301
+
302
+ `ip_firewall = true` (Cloudflare's L3/L4 DDoS) and
303
+ `argo_smart_routing = true` are on by default. `proxy_protocol = "v1"`
304
+ forwards the **real client IP** to the origin so the container's
305
+ per-IP admission gate sees the actual source, not Spectrum's address.
306
+
307
+ ---
308
+
309
+ ## 8. Persistence & state survival
310
+
311
+ The origin is a **stateful process** — a hard requirement that the wss
312
+ path (Durable Objects) does not have. Two layers of survival:
313
+
314
+ 1. **Across chunks within a connection** — the `TcpByteStreamTransport`
315
+ buffers partial lines in memory; a chunk may split an IRC command at
316
+ an arbitrary byte boundary and the next chunk completes it. This is
317
+ all in-process; nothing to configure.
318
+ 2. **Across origin restarts** (crash, deploy, scale-to-zero) — every
319
+ live TCP connection **drops**; clients must reconnect. But the
320
+ **channel topology** (topics, modes, ban masks) is snapshotted to
321
+ `PERSISTENCE_PATH` (`/data/state.json`, a `/data` volume in the
322
+ Dockerfile) on an interval (`SNAPSHOT_INTERVAL_MS`) and at graceful
323
+ shutdown, then restored on boot. Member rosters are **not** persisted
324
+ — they reference ConnIds that are stale after a restart, so a
325
+ reconnecting client rejoins an otherwise-empty channel.
326
+
327
+ > **Operational caveat:** the snapshot is per-instance. With
328
+ > `max_instances > 1`, each instance owns disjoint channels (no
329
+ > cross-instance state share). For a single coordinated channel space,
330
+ > keep `max_instances = 1` until the Durable-Object-backed persistent
331
+ > variant lands (ADR-009 "Negative", follow-up to this ticket).
332
+
333
+ ---
334
+
335
+ ## 9. mTLS (SASL EXTERNAL)
336
+
337
+ SASL `EXTERNAL` needs the client certificate to reach the application
338
+ layer (ADR-006, TICKET-054). On the TCP path this is the TLS handshake:
339
+
340
+ - **Flexible mode (default):** Spectrum terminates TLS; the origin sees
341
+ plaintext. The cert is consumed at the edge and the app only gets a
342
+ verified subject via Cloudflare's mTLS config — SASL `EXTERNAL` is
343
+ **not** available in flexible mode.
344
+ - **Full mode:** set `tls_mode = "full"` and configure the origin to
345
+ present its own certificate; configure Spectrum/API Shield mTLS with
346
+ your CA pool. The verified client-cert subject is then exposed to the
347
+ origin, feeding the `MtlsIdentityProvider` port → `AccountStore` →
348
+ `AUTHENTICATE EXTERNAL` succeeds (`903`) or fails (`904`).
349
+
350
+ Cross-reference: TICKET-054 (mTLS support), ADR-006 (SASL scope). The wss
351
+ path achieves the same via CF API Shield mTLS (see
352
+ `Cloudflare-Deployment-Guide.md`).
353
+
354
+ ---
355
+
356
+ ## 10. Secrets
357
+
358
+ The container consumes **no Cloudflare secret bindings** for v1; oper
359
+ and SASL credentials ship as `[vars]` (`OPER_*`, `SASL_ACCOUNTS`). When
360
+ strong-secret handling lands, set them via:
361
+
362
+ ```bash
363
+ wrangler secret put OPER_PASSWORD --env staging
364
+ ```
365
+
366
+ CI uses these **GitHub Actions secrets/variables** (configure under
367
+ Settings → Secrets and variables → Actions):
368
+
369
+ | Secret / var | Used by | Purpose |
370
+ |--------------------------|------------------------|--------------------------------------------|
371
+ | `CLOUDFLARE_API_TOKEN` | `deploy-cf-tcp.yml` | `wrangler deploy` + terraform (`TF_VAR_*`).|
372
+ | `CLOUDFLARE_ACCOUNT_ID` | `deploy-cf-tcp.yml` | Account context. |
373
+ | `CF_ZONE_ID` | `deploy-cf-tcp.yml` | `TF_VAR_zone_id`. |
374
+ | `CF_TCP_ORIGIN_ADDRESS` | `deploy-cf-tcp.yml` | `TF_VAR_origin_address`. |
375
+ | `CF_TCP_SMOKE_HOST` (var)| `deploy-cf-tcp.yml` | Host the TLS smoke step connects to. |
376
+
377
+ **Never** commit credentials to `wrangler.toml` or terraform `.tfvars`.
378
+
379
+ ---
380
+
381
+ ## 11. CI/CD
382
+
383
+ `.github/workflows/deploy-cf-tcp.yml` runs alongside `deploy-cf.yml`
384
+ (the wss path). The two are independent — a deployment can enable
385
+ either or both.
386
+
387
+ Triggers: push to `main` touching `apps/cf-tcp-container/**` or the
388
+ workflow itself, plus `workflow_dispatch`. Steps (in order):
389
+
390
+ 1. Checkout → pnpm 11 + Node 24 → `pnpm install --frozen-lockfile`.
391
+ 2. `pnpm build`, `pnpm typecheck`, `pnpm --filter cf-tcp-container test`.
392
+ 3. `wrangler deploy --env staging` (container).
393
+ 4. `terraform init && terraform apply -auto-approve` (Spectrum).
394
+ 5. TLS smoke (`openssl s_client`), gated on `CF_TCP_SMOKE_HOST`.
395
+
396
+ Concurrency is serialized via `concurrency.group: cf-tcp-staging`.
397
+
398
+ There is **no auto-deploy to production**; production is manual
399
+ (`pnpm deploy:cf-tcp:prod` + `terraform apply` for the prod hostname).
400
+
401
+ ---
402
+
403
+ ## 12. Cost notes (versus the wss path)
404
+
405
+ The TCP path is materially more expensive to operate than the wss path
406
+ because of the stateful origin:
407
+
408
+ | Dimension | wss path (Workers + DO) | TCP path (Spectrum + Container) |
409
+ |------------------------|------------------------------------|--------------------------------------------|
410
+ | **Plan tier** | Workers Paid ($5/mo) suffices. | **Enterprise required** (Spectrum). |
411
+ | **Idle connections** | Hibernation → ~zero memory cost. | Origin holds every socket; `sleep_after` only helps when *all* connections are idle. |
412
+ | **Compute** | Per-request (WebSocket frames). | **GB-second** of running container time. |
413
+ | **State durability** | DO storage (managed). | Container volume snapshot (self-managed). |
414
+ | **DDoS** | Cloudflare built-in. | Spectrum `ip_firewall` + Argo. |
415
+
416
+ Recommendation: keep the wss path as the default and only enable the TCP
417
+ path when you specifically need RFC-compliant clients or mTLS-via-TLS.
418
+ Most modern clients (WeeChat, IRCCloud, TheLounge) work over wss.
419
+
420
+ ---
421
+
422
+ ## 13. Troubleshooting
423
+
424
+ ### 13.1 `terraform apply` fails: `Spectrum app creation not permitted`
425
+
426
+ Spectrum is Enterprise-only. The API token's account/zone must be on an
427
+ Enterprise plan. Confirm in the dashboard (Network → Spectrum). If the
428
+ zone is not Enterprise, use the wss path.
429
+
430
+ ### 13.2 Client connects to :6697 but gets no `001`
431
+
432
+ The TLS handshake completed (Spectrum is up) but the origin did not
433
+ respond. Causes:
434
+
435
+ - The container is scaled to zero (`sleep_after`) and the cold start is
436
+ slow — wait a few seconds and retry.
437
+ - `CF_TCP_ORIGIN_ADDRESS` points at the wrong origin. It must match the
438
+ address printed by `wrangler deploy` (§5.1).
439
+ - The origin crashed on boot. Check `wrangler tail` for the container
440
+ (or container logs in the dashboard). A common cause is an invalid
441
+ `MOTD_LINES` or a malformed `SASL_ACCOUNTS` line.
442
+
443
+ ### 13.3 Channel topic/modes lost after a restart
444
+
445
+ `PERSISTENCE_PATH` is unset or the volume is not attached. Confirm
446
+ `PERSISTENCE_PATH=/data/state.json` in `[vars]` and that the `/data`
447
+ volume (`VOLUME ["/data"]` in the `Dockerfile`) is mounted on the
448
+ container platform. Without it the snapshot is written to ephemeral
449
+ storage and lost.
450
+
451
+ ### 13.4 Client IP seen as Spectrum's address
452
+
453
+ `proxy_protocol = "v1"` must be set in `spectrum.tf` (it is by default).
454
+ If you overrode it, the origin's per-IP admission gate sees Spectrum's
455
+ egress IP for every client and will wrongly throttle/cap.
456
+
457
+ ### 13.5 Messages split across chunks get lost
458
+
459
+ This is a transport-layer bug, not a config issue — the
460
+ `TcpByteStreamTransport` is contract-tested for chunk boundaries
461
+ (TICKET-053 / TICKET-057). If you see it, run
462
+ `pnpm --filter @serverless-ircd/cf-tcp-container test` and the shared
463
+ transport suite; a regression there will fail.
464
+
465
+ ### 13.6 `wrangler deploy` fails: `Containers beta not enabled`
466
+
467
+ Cloudflare Containers is in beta. Request access in the dashboard
468
+ (Workers & Pages → Containers). Until granted, only the local Docker
469
+ path (§4) and the wss transport work.
470
+
471
+ ---
472
+
473
+ ## 14. Quick reference
474
+
475
+ ```bash
476
+ # One-time account setup
477
+ npx wrangler login # or set CLOUDFLARE_API_TOKEN
478
+ terraform version # >= 1.5
479
+
480
+ # Local dev (plaintext :6667, no Spectrum)
481
+ pnpm build
482
+ pnpm --filter @serverless-ircd/cf-tcp-container start
483
+
484
+ # Staging
485
+ pnpm deploy:cf-tcp:staging # container
486
+ cd apps/cf-tcp-container/terraform && \
487
+ terraform apply -var zone_id=$CF_ZONE_ID \
488
+ -var origin_address=$CF_TCP_ORIGIN_ADDRESS \
489
+ -var hostname=irc-staging.example.com # Spectrum
490
+
491
+ # Production (manual)
492
+ pnpm deploy:cf-tcp:prod
493
+ cd apps/cf-tcp-container/terraform && terraform apply ... # prod hostname
494
+
495
+ # Tests
496
+ pnpm --filter @serverless-ircd/cf-tcp-container test
497
+ ```
498
+
499
+ Key files:
500
+
501
+ | Path | What |
502
+ |-------------------------------------------------|--------------------------------------------|
503
+ | `apps/cf-tcp-container/src/main.ts` | Container entry point. |
504
+ | `apps/cf-tcp-container/src/container-server.ts` | TCP server + actor wiring + persistence. |
505
+ | `apps/cf-tcp-container/src/config-loader.ts` | Env → `ServerConfig` + container knobs. |
506
+ | `apps/cf-tcp-container/src/persistence.ts` | Channel-topology snapshot/restore. |
507
+ | `apps/cf-tcp-container/wrangler.toml` | Container deploy config. |
508
+ | `apps/cf-tcp-container/Dockerfile` | Image (node:24, `/data` volume, :6667). |
509
+ | `apps/cf-tcp-container/terraform/spectrum.tf` | Spectrum TLS :6697 → origin :6667. |
510
+ | `.github/workflows/deploy-cf-tcp.yml` | Staging deploy + Spectrum + smoke CI. |
package/docs/Home.md ADDED
@@ -0,0 +1,15 @@
1
+ Welcome to the Wiki.
2
+
3
+ ## Pages
4
+
5
+ - [AWS Adapter Architecture](AWS-Adapter-Architecture)
6
+ - [AWS Deployment Guide](AWS-Deployment)
7
+ - [AWS TCP+TLS Deployment Guide](AWS-TCP-Deployment)
8
+ - [Cloudflare Deployment Guide](Cloudflare-Deployment-Guide)
9
+ - [Cloudflare TCP+TLS Deployment Guide](Cloudflare-TCP-Deployment)
10
+ - [Observability](Observability)
11
+ - [IRCv3 WebSocket Extension Plan](PlanIRCv3Websocket)
12
+ - [Web Client Plan](PlanWebClient)
13
+ - [Extensions Plan](PlanExtensions)
14
+ - [Release Process](Release-Process)
15
+ - [ADR Index](ADR-Index)
@@ -0,0 +1,87 @@
1
+ # Observability
2
+
3
+ The server ships a structured-logger port that every adapter uses to emit
4
+ JSON log lines carrying a per-request `traceId` and per-connection
5
+ `connectionId`. The canonical event names are the contract dashboards and
6
+ log queries are built against.
7
+
8
+ ## Log contract
9
+
10
+ Every record is one physical line of JSON with at least:
11
+
12
+ | Field | Source |
13
+ |----------------|-------------------------------------------------|
14
+ | `ts` | ISO-8601 timestamp from the injected `Clock`. |
15
+ | `level` | `debug` / `info` / `warn` / `error`. |
16
+ | `msg` | Event name (see below). |
17
+ | `traceId` | Per-request id; fresh per inbound frame. |
18
+ | `connectionId` | The bound connection's id (`ConnId`). |
19
+
20
+ Additional structured fields appear per event. The `msg` catalogue:
21
+
22
+ | `msg` | Level | Emitted by | Notes |
23
+ |--------------------|-------|-------------------------|------------------------------------------------------|
24
+ | `frame.receive` | debug | `ConnectionActor` | One per parsed IRC line. Fields: `command`, `lines`. |
25
+ | `frame.parse-error`| warn | `ConnectionActor` | A line failed to parse. Fields: `line`, `token`. |
26
+ | `dispatch` | debug | `dispatch()` | Before interpreting an effect list. Fields: `effects` (array of tags), `histogram`. |
27
+ | `dispatch.error` | warn | `dispatch()` | A runtime method rejected. Fields: `effect`, `err`. |
28
+
29
+ Adapters may emit their own events (e.g. `local-cli listening`, `shutdown`)
30
+ following the same shape.
31
+
32
+ ## Cloudflare Workers
33
+
34
+ The `apps/cf-worker/wrangler.toml` enables `[observability]` so every
35
+ `console.*` line emitted by the `ConsoleLogger` inside `ConnectionDO` is
36
+ captured by Workers Analytics.
37
+
38
+ - Tail the live feed: `pnpm --filter @serverless-ircd/cf-worker tail`
39
+ - Filter by connection: `connectionId = "<DO hex id>"`
40
+ - Filter by trace: `traceId = "<uuid>"`
41
+ - Errors only: `level = "error" or msg = "dispatch.error"`
42
+
43
+ Workers Analytics dashboards can be built on the Cloudflare dashboard
44
+ under **Workers & Pages → <Worker> → Observability**.
45
+
46
+ ## AWS (Lambda + API Gateway WebSocket)
47
+
48
+ The Lambda handler writes to CloudWatch Logs via the global `console`.
49
+ Each invocation emits one JSON line per log record; the log group name
50
+ follows the standard `/aws/lambda/<function-name>` convention.
51
+
52
+ A starter CloudWatch dashboard is checked in at
53
+ `dashboards/cloudwatch-irc.json`. Provision it via:
54
+
55
+ ```sh
56
+ aws cloudwatch put-dashboard \
57
+ --dashboard-name ServerlessIRCd-staging \
58
+ --dashboard-body file://dashboards/cloudwatch-irc.json
59
+ ```
60
+
61
+ The dashboard variables (`API_ID`, `LAMBDA_NAME`, `LOG_GROUP_ARN`,
62
+ `AWS_REGION`) are templated by CloudWatch's dashboard variables feature —
63
+ set them once in the dashboard UI after import.
64
+
65
+ Sample CloudWatch Logs Insights query — count dispatched effects by tag
66
+ over the last hour:
67
+
68
+ ```
69
+ fields @timestamp, msg, fields.histogram
70
+ | filter msg = "dispatch"
71
+ | sort @timestamp desc
72
+ ```
73
+
74
+ ## Local CLI
75
+
76
+ The local-cli server uses the same `ConsoleLogger` so the structured
77
+ stream is visible while developing:
78
+
79
+ ```
80
+ {"ts":"2026-07-22T12:34:56.789Z","level":"debug","msg":"frame.receive","traceId":"…","connectionId":"…","command":"PRIVMSG","lines":1}
81
+ ```
82
+
83
+ Filter with `jq`:
84
+
85
+ ```
86
+ pnpm --filter local-cli start | jq 'select(.msg=="dispatch.error")'
87
+ ```