serverless-ircd 0.3.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 (159) 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 +174 -18
  8. package/README.md +63 -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/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 +20 -3
  38. package/package.json +14 -10
  39. package/packages/aws-adapter/package.json +3 -3
  40. package/packages/aws-adapter/src/account-store.ts +1 -1
  41. package/packages/aws-adapter/src/admission.ts +74 -0
  42. package/packages/aws-adapter/src/aws-runtime.ts +6 -4
  43. package/packages/aws-adapter/src/config-loader.ts +32 -0
  44. package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
  45. package/packages/aws-adapter/src/handlers/connect.ts +64 -8
  46. package/packages/aws-adapter/src/handlers/default.ts +35 -2
  47. package/packages/aws-adapter/src/handlers/index.ts +69 -3
  48. package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
  49. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  50. package/packages/aws-adapter/src/index.ts +6 -0
  51. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
  52. package/packages/aws-adapter/tests/account-store.test.ts +19 -20
  53. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  54. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  55. package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
  56. package/packages/aws-adapter/tests/connect.test.ts +78 -0
  57. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
  58. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  59. package/packages/aws-adapter/tests/handlers.test.ts +154 -53
  60. package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
  61. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  62. package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
  63. package/packages/aws-adapter/tests/transactions.test.ts +25 -20
  64. package/packages/cf-adapter/package.json +1 -1
  65. package/packages/cf-adapter/src/cf-runtime.ts +2 -4
  66. package/packages/cf-adapter/src/config-loader.ts +33 -0
  67. package/packages/cf-adapter/src/connection-do.ts +111 -46
  68. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  69. package/packages/cf-adapter/src/env.ts +33 -9
  70. package/packages/cf-adapter/src/index.ts +9 -8
  71. package/packages/cf-adapter/tests/cf-harness.ts +11 -1
  72. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
  73. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  74. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  75. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  76. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  77. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  78. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  79. package/packages/cf-adapter/tests/worker/main.ts +8 -1
  80. package/packages/cf-adapter/wrangler.test.toml +8 -0
  81. package/packages/in-memory-runtime/package.json +1 -1
  82. package/packages/irc-core/package.json +6 -1
  83. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  84. package/packages/irc-core/src/caps/capabilities.ts +23 -2
  85. package/packages/irc-core/src/cloak.ts +1 -1
  86. package/packages/irc-core/src/commands/cap.ts +8 -1
  87. package/packages/irc-core/src/commands/index.ts +9 -0
  88. package/packages/irc-core/src/commands/ison.ts +61 -0
  89. package/packages/irc-core/src/commands/quit.ts +12 -0
  90. package/packages/irc-core/src/commands/registration.ts +18 -12
  91. package/packages/irc-core/src/commands/sasl.ts +72 -9
  92. package/packages/irc-core/src/commands/server-info.ts +129 -0
  93. package/packages/irc-core/src/commands/userhost.ts +84 -0
  94. package/packages/irc-core/src/commands/whowas.ts +113 -0
  95. package/packages/irc-core/src/config.ts +58 -0
  96. package/packages/irc-core/src/credential-hashing.ts +124 -0
  97. package/packages/irc-core/src/effects.ts +6 -29
  98. package/packages/irc-core/src/index.ts +1 -0
  99. package/packages/irc-core/src/ports.ts +181 -12
  100. package/packages/irc-core/src/protocol/numerics.ts +14 -8
  101. package/packages/irc-core/src/types.ts +38 -1
  102. package/packages/irc-core/tests/account-store.test.ts +45 -2
  103. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  104. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  105. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  106. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  107. package/packages/irc-core/tests/commands/registration.test.ts +151 -6
  108. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  109. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  110. package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
  111. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  112. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  113. package/packages/irc-core/tests/config.test.ts +95 -1
  114. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  115. package/packages/irc-core/tests/effects.test.ts +0 -27
  116. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  117. package/packages/irc-core/tests/numerics.test.ts +12 -0
  118. package/packages/irc-core/tests/types.test.ts +35 -1
  119. package/packages/irc-core/tsconfig.build.json +1 -1
  120. package/packages/irc-core/tsconfig.test.json +1 -1
  121. package/packages/irc-server/package.json +1 -1
  122. package/packages/irc-server/src/actor.ts +158 -14
  123. package/packages/irc-server/src/dispatch.ts +0 -3
  124. package/packages/irc-server/src/index.ts +10 -2
  125. package/packages/irc-server/src/routing.ts +12 -0
  126. package/packages/irc-server/src/transport.ts +101 -0
  127. package/packages/irc-server/tests/actor.test.ts +400 -3
  128. package/packages/irc-server/tests/dispatch.test.ts +0 -17
  129. package/packages/irc-server/tests/routing.test.ts +4 -0
  130. package/packages/irc-server/tests/transport.test.ts +230 -0
  131. package/packages/irc-test-support/package.json +1 -1
  132. package/packages/irc-test-support/src/harness.ts +44 -9
  133. package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
  134. package/packages/irc-test-support/src/index.ts +3 -0
  135. package/packages/irc-test-support/src/scenarios.ts +132 -2
  136. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  137. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  138. package/pnpm-workspace.yaml +9 -1
  139. package/tools/ci-hardening/package.json +1 -1
  140. package/tools/package.json +4 -0
  141. package/tools/seed-aws-accounts.ts +5 -6
  142. package/tools/seed-cf-accounts.ts +104 -0
  143. package/tools/tcp-ws-forwarder/package.json +1 -1
  144. package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
  145. package/docs/ADR-002-location-of-authority.md +0 -82
  146. package/docs/ADR-003-durable-object-sharding.md +0 -93
  147. package/docs/ADR-004-dynamodb-schema.md +0 -96
  148. package/docs/ADR-005-wss-only-transport-v1.md +0 -83
  149. package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
  150. package/docs/ADR-007-deterministic-ports.md +0 -82
  151. package/docs/ADR-008-monorepo-tooling.md +0 -60
  152. package/docs/AWS-Adapter-Architecture.md +0 -496
  153. package/docs/AWS-Deployment.md +0 -1186
  154. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  155. package/docs/Home.md +0 -11
  156. package/docs/Observability.md +0 -87
  157. package/docs/PlanIRCv3Websocket.md +0 -489
  158. package/docs/PlanWebClient.md +0 -451
  159. 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,164 @@ 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
+
13
171
  ## [0.3.0] - 2026-07-25
14
172
 
15
173
  ### Added — Operator authentication (`@serverless-ircd/irc-core`)
@@ -378,31 +536,29 @@ manual per `docs/release.md` §8.3.
378
536
 
379
537
  ### Known limitations
380
538
 
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.
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.
385
542
  - **No SASL.** `CAP sasl` is advertised in the capability list but
386
543
  `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.
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.
390
547
  - **No server-password enforcement, rate limits, cloaking, or oper
391
- credentials** (TICKET-046).
548
+ credentials.**
392
549
  - **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`.
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`.
397
553
  - **No CI coverage gate.** Coverage is computed and uploaded as an
398
- artifact, but no threshold is enforced (TICKET-048). Inspect
554
+ artifact, but no threshold is enforced. Inspect
399
555
  `packages/*/coverage/` before deploying.
400
556
  - **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.
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.
406
562
 
407
563
  ### Security
408
564
 
package/README.md CHANGED
@@ -4,22 +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.3.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. v0.2.0 delivered SASL (`PLAIN`), token-bucket
14
- > flood control, server-password enforcement, hostmask cloaking,
15
- > connection admission, and IRCv3 chat history; v0.3.0 adds **OPER**
16
- > credential auth, the IRCv3 **TAGMSG** command, **RFC 1459
17
- > case-mapping**, a real **MOTD** in the registration welcome, a
18
- > **ChannelRegistryDO** (4th Cloudflare Durable Object) plus a
19
- > Cloudflare config loader, and **SASL account persistence** in
20
- > DynamoDB (scrypt hashes) with a `seed-aws-accounts` provisioning
21
- > tool. Remaining 0.x work: SASL `EXTERNAL` (mTLS) and production TLS
22
- > transport. See `CHANGELOG.md` for the per-release manifests.
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.
23
24
 
24
25
  ---
25
26
 
@@ -70,6 +71,16 @@ Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
70
71
  `packages/in-memory-runtime` is the reference implementation of
71
72
  `IrcRuntime`, used by integration tests and the local CLI server.
72
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
+
73
84
  ### The key idea: pure reducers + location-of-authority
74
85
 
75
86
  Each command handler is a pure function:
@@ -111,12 +122,14 @@ ServerlessIRCd/
111
122
  │ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime
112
123
  ├── apps/
113
124
  │ ├── cf-worker/ worker entry, DO migrations, bindings
114
- │ ├── 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)
115
127
  │ └── local-cli/ runnable WS + TCP server using in-memory-runtime
116
128
  ├── tools/
117
129
  │ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
118
130
  │ ├── ci-hardening/ coverage-gate + mutation-config validators
119
- └── seed-aws-accounts.ts scrypt-hash SASL PLAIN accounts into DynamoDB
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
120
133
  ├── pnpm-workspace.yaml turbo.json tsconfig.base.json
121
134
  └── README.md CHANGELOG.md
122
135
  ```
@@ -127,7 +140,7 @@ ServerlessIRCd/
127
140
 
128
141
  | Concern | Choice |
129
142
  |------------------|---------------------------------------------------|
130
- | Runtime | Node ≥ 20, ES2022+ |
143
+ | Runtime | Node ≥ 24, ES2022+ |
131
144
  | Package mgr | pnpm workspaces |
132
145
  | Build cache | turbo |
133
146
  | Language | TypeScript (strict, `exactOptionalPropertyTypes`) |
@@ -141,9 +154,12 @@ ServerlessIRCd/
141
154
 
142
155
  ## Getting started
143
156
 
144
- Requires Node ≥ 20 and pnpm 9.
157
+ Requires Node ≥ 24 and pnpm 11. Enable corepack so the `packageManager`
158
+ pin is resolved automatically:
145
159
 
146
160
  ```bash
161
+ corepack enable # lets the pinned pnpm@11 run on any Node ≥ 24
162
+
147
163
  pnpm install # install workspace deps
148
164
 
149
165
  pnpm build # build all packages (turbo)
@@ -322,10 +338,16 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
322
338
  - **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
323
339
  ✅ landed in **v0.2.0** (staging auto-deploy via CI; prod deploy is manual).
324
340
  - **Phase 5** — Observability, security hardening, config, CI gates.
325
- ✅ landed (logger port, server-password, cloaking, admission limits,
326
- coverage + mutation gates, OPER credential auth, and the Cloudflare
327
- config loader all shipped across v0.2.0–v0.3.0).
328
- - **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.
329
351
 
330
352
  ---
331
353
 
@@ -333,25 +355,31 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
333
355
 
334
356
  **Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
335
357
  `PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
336
- channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD`,
337
- `AWAY`, `OPER` (credential auth → `o` user mode).
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.)
338
363
 
339
364
  **Channel modes:** `o v b i k l t n m s p`.
340
365
  **User modes:** `i`, `o` (local only), `w`, `s`.
341
366
 
342
367
  **IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
343
368
  `TAGMSG` command), `server-time`, `account-tag`, `echo-message`, `batch`,
344
- `sasl` (`PLAIN` implemented, `EXTERNAL` reserved pending mTLS),
369
+ `sasl` (`PLAIN` always; `EXTERNAL` via mTLS when a client-cert trust store
370
+ is bound),
345
371
  `multi-prefix`, `away-notify`, `chghost`, `invite-notify`, `extended-join`,
346
372
  `draft/chathistory`, `safelist`. Case-insensitive nick/channel comparison
347
373
  uses **RFC 1459** case-mapping (advertised via `005 CASEMAPPING=rfc1459`).
348
374
 
349
375
  **Transport:** deployed stacks (Cloudflare Workers, AWS API Gateway) speak
350
- WebSocket text frames only (one IRC message per frame, with tolerance for
351
- `\r\n`-joined frames). The local CLI additionally binds a plain RFC TCP
352
- listener for direct IRC-client access. Stock TCP IRC clients reach a deployed
353
- WebSocket endpoint through the local
354
- [`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder).
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.
355
383
 
356
384
  ---
357
385
 
@@ -359,8 +387,13 @@ WebSocket endpoint through the local
359
387
 
360
388
  - `CHANGELOG.md` — per-release manifests (Keep a Changelog format), including
361
389
  the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
362
- gates) and v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
363
- ChannelRegistryDO, SASL account persistence).
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.
364
397
  - `README.md` in each `packages/*` and `apps/*` — per-package notes (e.g.
365
398
  `packages/aws-adapter/README.md` for the DynamoDB-Local test setup,
366
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/aws-stack",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "description": "AWS CDK v2 stack: API Gateway v2 WebSocket API, Lambda, DynamoDB tables, least-privilege IAM",
6
6
  "license": "BSD-3-Clause",
@@ -19,8 +19,8 @@
19
19
  "smoke:staging": "node scripts/smoke.mjs"
20
20
  },
21
21
  "devDependencies": {
22
- "aws-cdk": "^2.160.0",
23
- "aws-cdk-lib": "^2.160.0",
22
+ "aws-cdk": "^2.262.0",
23
+ "aws-cdk-lib": "^2.262.0",
24
24
  "constructs": "^10.4.0",
25
25
  "@types/node": "^26.1.1",
26
26
  "@types/ws": "^8.5.13",
@@ -36,6 +36,6 @@
36
36
  "@serverless-ircd/aws-adapter": "workspace:*"
37
37
  },
38
38
  "engines": {
39
- "node": ">=20"
39
+ "node": ">=24"
40
40
  }
41
41
  }