serverless-ircd 0.10.0 → 0.11.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 (192) hide show
  1. package/.github/workflows/ci.yml +28 -0
  2. package/.github/workflows/deploy-cf-tcp.yml +26 -2
  3. package/.github/workflows/deploy-cf.yml +26 -0
  4. package/CHANGELOG.md +289 -0
  5. package/README.md +153 -20
  6. package/apps/aws-stack/bin/aws.ts +36 -0
  7. package/apps/aws-stack/package.json +1 -1
  8. package/apps/aws-stack/src/aws-stack.ts +221 -15
  9. package/apps/aws-stack/tests/stack.test.ts +450 -16
  10. package/apps/cf-tcp-container/Dockerfile +37 -5
  11. package/apps/cf-tcp-container/package.json +7 -2
  12. package/apps/cf-tcp-container/src/config-loader.ts +113 -2
  13. package/apps/cf-tcp-container/src/container-server.ts +256 -79
  14. package/apps/cf-tcp-container/src/main.ts +22 -7
  15. package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
  16. package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
  17. package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
  18. package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
  19. package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
  20. package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
  21. package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
  22. package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
  23. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
  24. package/apps/cf-tcp-container/wrangler.toml +17 -4
  25. package/apps/cf-worker/package.json +2 -2
  26. package/apps/cf-worker/src/worker.ts +77 -5
  27. package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
  28. package/apps/cf-worker/tests/smoke.test.ts +4 -0
  29. package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
  30. package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
  31. package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
  32. package/apps/cf-worker/wrangler.test.toml +15 -1
  33. package/apps/cf-worker/wrangler.toml +86 -9
  34. package/apps/local-cli/package.json +1 -1
  35. package/apps/local-cli/src/config-loader.ts +14 -2
  36. package/apps/local-cli/src/line-scanner.ts +26 -0
  37. package/apps/local-cli/src/server.ts +23 -2
  38. package/apps/local-cli/tests/line-scanner.test.ts +64 -0
  39. package/apps/local-cli/tests/tcp.test.ts +29 -0
  40. package/apps/web/package.json +1 -1
  41. package/docs/AWS-Deployment.md +123 -22
  42. package/docs/AWS-TCP-Deployment.md +37 -2
  43. package/docs/Chat-History.md +55 -0
  44. package/docs/Cloudflare-Deployment-Guide.md +9 -2
  45. package/docs/Cloudflare-TCP-Deployment.md +135 -52
  46. package/docs/SASL-EXTERNAL.md +175 -0
  47. package/package.json +3 -3
  48. package/packages/aws-adapter/package.json +1 -1
  49. package/packages/aws-adapter/src/admission.ts +28 -13
  50. package/packages/aws-adapter/src/aws-runtime.ts +30 -3
  51. package/packages/aws-adapter/src/cdk-table-defs.ts +34 -6
  52. package/packages/aws-adapter/src/config-loader.ts +134 -6
  53. package/packages/aws-adapter/src/dynamo-services-store.ts +12 -0
  54. package/packages/aws-adapter/src/handlers/connect.ts +47 -1
  55. package/packages/aws-adapter/src/handlers/default.ts +95 -6
  56. package/packages/aws-adapter/src/handlers/index.ts +31 -2
  57. package/packages/aws-adapter/src/handlers/nlb-stream.ts +132 -8
  58. package/packages/aws-adapter/src/ip-admission.ts +79 -0
  59. package/packages/aws-adapter/src/serialize.ts +8 -0
  60. package/packages/aws-adapter/src/tables.ts +9 -0
  61. package/packages/aws-adapter/tests/admission.test.ts +60 -2
  62. package/packages/aws-adapter/tests/aws-harness.ts +23 -1
  63. package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
  64. package/packages/aws-adapter/tests/config-loader.test.ts +151 -0
  65. package/packages/aws-adapter/tests/connect.test.ts +199 -2
  66. package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
  67. package/packages/aws-adapter/tests/default-occ.test.ts +10 -3
  68. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +123 -1
  69. package/packages/aws-adapter/tests/handlers.test.ts +57 -1
  70. package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
  71. package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
  72. package/packages/cf-adapter/package.json +1 -1
  73. package/packages/cf-adapter/src/cf-runtime.ts +48 -9
  74. package/packages/cf-adapter/src/config-loader.ts +133 -8
  75. package/packages/cf-adapter/src/connection-do.ts +154 -21
  76. package/packages/cf-adapter/src/counter-do.ts +142 -0
  77. package/packages/cf-adapter/src/d1-services-store.ts +47 -5
  78. package/packages/cf-adapter/src/env.ts +88 -0
  79. package/packages/cf-adapter/src/index.ts +17 -1
  80. package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
  81. package/packages/cf-adapter/tests/cf-runtime.test.ts +104 -15
  82. package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
  83. package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
  84. package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
  85. package/packages/cf-adapter/tests/connection-do-pure.test.ts +74 -5
  86. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
  87. package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
  88. package/packages/cf-adapter/tests/d1-services-store.test.ts +192 -1
  89. package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
  90. package/packages/cf-adapter/tests/worker/main.ts +4 -0
  91. package/packages/cf-adapter/wrangler.test.toml +18 -1
  92. package/packages/in-memory-runtime/package.json +1 -1
  93. package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
  94. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
  95. package/packages/irc-core/package.json +1 -1
  96. package/packages/irc-core/src/caps/capabilities.ts +20 -10
  97. package/packages/irc-core/src/certfp.ts +178 -0
  98. package/packages/irc-core/src/commands/cap.ts +10 -2
  99. package/packages/irc-core/src/commands/chanserv.ts +117 -14
  100. package/packages/irc-core/src/commands/chathistory.ts +13 -5
  101. package/packages/irc-core/src/commands/hostserv.ts +84 -8
  102. package/packages/irc-core/src/commands/index.ts +2 -1
  103. package/packages/irc-core/src/commands/invite.ts +1 -7
  104. package/packages/irc-core/src/commands/join.ts +1 -16
  105. package/packages/irc-core/src/commands/kick.ts +1 -8
  106. package/packages/irc-core/src/commands/list.ts +1 -8
  107. package/packages/irc-core/src/commands/mode.ts +1 -8
  108. package/packages/irc-core/src/commands/multiline.ts +4 -10
  109. package/packages/irc-core/src/commands/names.ts +53 -13
  110. package/packages/irc-core/src/commands/nickserv.ts +40 -1
  111. package/packages/irc-core/src/commands/oper.ts +361 -8
  112. package/packages/irc-core/src/commands/part.ts +4 -10
  113. package/packages/irc-core/src/commands/privmsg.ts +8 -4
  114. package/packages/irc-core/src/commands/registration.ts +146 -2
  115. package/packages/irc-core/src/commands/sasl.ts +136 -19
  116. package/packages/irc-core/src/commands/topic.ts +10 -12
  117. package/packages/irc-core/src/commands/who.ts +1 -8
  118. package/packages/irc-core/src/config.ts +393 -20
  119. package/packages/irc-core/src/effects.ts +24 -0
  120. package/packages/irc-core/src/flood-control.ts +10 -10
  121. package/packages/irc-core/src/frame-rate-limit.ts +82 -0
  122. package/packages/irc-core/src/index.ts +8 -0
  123. package/packages/irc-core/src/oper-hashing.ts +43 -0
  124. package/packages/irc-core/src/oper-lockout.ts +87 -0
  125. package/packages/irc-core/src/ports.ts +395 -36
  126. package/packages/irc-core/src/protocol/bytes.ts +65 -0
  127. package/packages/irc-core/src/protocol/channel-name.ts +37 -0
  128. package/packages/irc-core/src/protocol/index.ts +12 -1
  129. package/packages/irc-core/src/protocol/outbound.ts +43 -10
  130. package/packages/irc-core/src/protocol/parser.ts +79 -10
  131. package/packages/irc-core/src/state/connection.ts +13 -0
  132. package/packages/irc-core/src/types.ts +228 -13
  133. package/packages/irc-core/src/ws-framing.ts +5 -4
  134. package/packages/irc-core/tests/bytes.test.ts +89 -0
  135. package/packages/irc-core/tests/certfp.test.ts +117 -0
  136. package/packages/irc-core/tests/commands/cap.test.ts +76 -2
  137. package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
  138. package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
  139. package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
  140. package/packages/irc-core/tests/commands/join.test.ts +78 -1
  141. package/packages/irc-core/tests/commands/names.test.ts +193 -0
  142. package/packages/irc-core/tests/commands/nickserv.test.ts +182 -2
  143. package/packages/irc-core/tests/commands/oper.test.ts +560 -2
  144. package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
  145. package/packages/irc-core/tests/commands/registration.test.ts +463 -1
  146. package/packages/irc-core/tests/commands/sasl.test.ts +596 -7
  147. package/packages/irc-core/tests/commands/topic.test.ts +137 -2
  148. package/packages/irc-core/tests/commands/unified-account.test.ts +2 -0
  149. package/packages/irc-core/tests/config.test.ts +534 -2
  150. package/packages/irc-core/tests/effects.test.ts +14 -0
  151. package/packages/irc-core/tests/flood-control.test.ts +29 -1
  152. package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
  153. package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
  154. package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
  155. package/packages/irc-core/tests/outbound.test.ts +148 -0
  156. package/packages/irc-core/tests/parser.test.ts +287 -5
  157. package/packages/irc-core/tests/persistent-services-store.test.ts +141 -0
  158. package/packages/irc-core/tests/ports.test.ts +99 -7
  159. package/packages/irc-core/tests/services-store.test.ts +376 -14
  160. package/packages/irc-core/tests/ws-framing.test.ts +45 -0
  161. package/packages/irc-server/package.json +1 -1
  162. package/packages/irc-server/src/actor.ts +123 -8
  163. package/packages/irc-server/src/dispatch.ts +1 -0
  164. package/packages/irc-server/src/index.ts +7 -0
  165. package/packages/irc-server/src/redact.ts +159 -0
  166. package/packages/irc-server/src/runtime.ts +14 -0
  167. package/packages/irc-server/src/transport.ts +28 -1
  168. package/packages/irc-server/tests/actor.test.ts +544 -7
  169. package/packages/irc-server/tests/dispatch.test.ts +31 -0
  170. package/packages/irc-server/tests/redact.test.ts +198 -0
  171. package/packages/irc-server/tests/runtime.test.ts +2 -0
  172. package/packages/irc-server/tests/transport.test.ts +66 -0
  173. package/packages/irc-test-support/package.json +1 -1
  174. package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
  175. package/scripts/package.json +1 -1
  176. package/tools/ci-hardening/package.json +2 -2
  177. package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
  178. package/tools/ci-hardening/src/cf-deploy.ts +118 -0
  179. package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
  180. package/tools/ci-hardening/src/env-var-drift.ts +192 -0
  181. package/tools/ci-hardening/src/hostname-guard.ts +11 -0
  182. package/tools/ci-hardening/src/index.ts +17 -0
  183. package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
  184. package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
  185. package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
  186. package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
  187. package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
  188. package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
  189. package/tools/ci-hardening/vitest.config.ts +5 -1
  190. package/tools/hash-oper-cred.ts +85 -0
  191. package/tools/load-test/package.json +1 -1
  192. package/tools/tcp-ws-forwarder/package.json +1 -1
@@ -124,6 +124,34 @@ jobs:
124
124
  # config) before it reaches main.
125
125
  run: pnpm --filter @serverless-ircd/ci-hardening test
126
126
 
127
+ - name: Container non-root smoke (gated on docker availability)
128
+ # Live proof that the cf-tcp-container image drops root: the
129
+ # runtime UID must be 1000 and /data must be writable by it.
130
+ # The static Dockerfile assertions in
131
+ # apps/cf-tcp-container/tests/dockerfile.test.ts always run in
132
+ # the test step; this smoke additionally builds and runs the
133
+ # real image. Gated because the self-hosted runner image ships
134
+ # without a docker daemon (see the DynamoDB Local notes above).
135
+ run: |
136
+ if ! command -v docker >/dev/null 2>&1 || ! timeout 15 docker info >/dev/null 2>&1; then
137
+ echo "docker not available — skipping live container smoke (static Dockerfile test covers it)"
138
+ exit 0
139
+ fi
140
+ docker build -f apps/cf-tcp-container/Dockerfile -t sirc-tcp-origin-smoke .
141
+ uid="$(docker run --rm sirc-tcp-origin-smoke id -u)"
142
+ echo "runtime UID: ${uid}"
143
+ if [ "${uid}" != "1000" ]; then
144
+ echo "FAIL: expected runtime UID 1000, got ${uid}"
145
+ exit 1
146
+ fi
147
+ docker run --rm sirc-tcp-origin-smoke \
148
+ node -e "const fs=require('fs');fs.writeFileSync('/data/.smoke','ok');fs.unlinkSync('/data/.smoke')"
149
+ if [ "$?" -ne 0 ]; then
150
+ echo "FAIL: /data not writable as UID 1000"
151
+ exit 1
152
+ fi
153
+ echo "SMOKE PASS: image runs as UID 1000 and /data is writable"
154
+
127
155
  - name: Upload coverage
128
156
  if: always()
129
157
  uses: actions/upload-artifact@v3
@@ -17,9 +17,22 @@ name: deploy-cf-tcp
17
17
  # CLOUDFLARE_ACCOUNT_ID Account ID for the target zone.
18
18
  # CF_ZONE_ID Zone ID for the Spectrum hostname.
19
19
  # CF_TCP_ORIGIN_ADDRESS Hostname/IP of the container origin.
20
+ #
21
+ # Inputs (prompted on workflow_dispatch):
22
+ # hostname REQUIRED. Public IRC hostname this deploy
23
+ # serves (e.g. irc.example.net). The pipeline
24
+ # fails loudly when unset and refuses to ship
25
+ # while it is still a repo placeholder
26
+ # (irc.example.com / irc.your-domain.invalid)
27
+ # — the guard step below enforces both.
20
28
 
21
29
  on:
22
30
  workflow_dispatch:
31
+ inputs:
32
+ hostname:
33
+ description: 'Public IRC hostname for this deploy (must not be a placeholder)'
34
+ required: true
35
+ type: string
23
36
 
24
37
  concurrency:
25
38
  group: cf-tcp-deploy
@@ -49,6 +62,16 @@ jobs:
49
62
  - name: Build workspace
50
63
  run: pnpm build
51
64
 
65
+ # Fail fast, before any deploy or terraform step, when the
66
+ # hostname input is unset or still a repo placeholder. Runs the
67
+ # same guard the deploy scripts use (tools/ci-hardening
68
+ # hostname-guard); the `::error::` message names the offending
69
+ # value and how to set a real hostname.
70
+ - name: 'Guard: reject placeholder hostnames'
71
+ env:
72
+ DEPLOY_HOSTNAME: ${{ inputs.hostname }}
73
+ run: node tools/ci-hardening/dist/hostname-guard.js "$DEPLOY_HOSTNAME"
74
+
52
75
  - name: Typecheck
53
76
  run: pnpm typecheck
54
77
 
@@ -60,14 +83,15 @@ jobs:
60
83
  env:
61
84
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
62
85
  CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
63
- run: wrangler deploy
86
+ SERVER_NAME: ${{ inputs.hostname }}
87
+ run: pnpm run deploy -- --var "SERVER_NAME:$SERVER_NAME"
64
88
 
65
89
  - name: Apply Spectrum TLS config
66
90
  working-directory: apps/cf-tcp-container/terraform
67
91
  env:
68
92
  TF_VAR_zone_id: ${{ secrets.CF_ZONE_ID }}
69
93
  TF_VAR_origin_address: ${{ secrets.CF_TCP_ORIGIN_ADDRESS }}
70
- TF_VAR_hostname: irc.example.com
94
+ TF_VAR_hostname: ${{ inputs.hostname }}
71
95
  run: |
72
96
  terraform init
73
97
  terraform apply -auto-approve
@@ -24,9 +24,23 @@ name: deploy-cf
24
24
  # plus the deployer's account subdomain. Override by setting
25
25
  # `CF_SMOKE_URL` in the repo Variables store when custom routes are
26
26
  # wired.
27
+ #
28
+ # Inputs (prompted on workflow_dispatch):
29
+ # hostname REQUIRED. Public IRC hostname this deploy serves
30
+ # (e.g. irc.example.net). Fails loudly when unset and
31
+ # refuses to ship while it is still a repo placeholder
32
+ # (irc.example.com / irc.your-domain.invalid) — the
33
+ # guard step below enforces both, and the deploy step
34
+ # overrides the shipped wrangler.toml `SERVER_NAME`
35
+ # default with the input via `--var`.
27
36
 
28
37
  on:
29
38
  workflow_dispatch:
39
+ inputs:
40
+ hostname:
41
+ description: 'Public IRC hostname for this deploy (must not be a placeholder)'
42
+ required: true
43
+ type: string
30
44
 
31
45
  # Prevent two deploys racing the same DO namespace.
32
46
  concurrency:
@@ -57,6 +71,14 @@ jobs:
57
71
  - name: Build workspace
58
72
  run: pnpm build
59
73
 
74
+ # Fail fast, before the deploy step, when the hostname input is
75
+ # unset or still a repo placeholder (same guard the deploy
76
+ # scripts use; tools/ci-hardening hostname-guard).
77
+ - name: 'Guard: reject placeholder hostnames'
78
+ env:
79
+ DEPLOY_HOSTNAME: ${{ inputs.hostname }}
80
+ run: node tools/ci-hardening/dist/hostname-guard.js "$DEPLOY_HOSTNAME"
81
+
60
82
  - name: Typecheck
61
83
  run: pnpm typecheck
62
84
 
@@ -68,6 +90,10 @@ jobs:
68
90
  env:
69
91
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
70
92
  CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
93
+ SERVER_NAME: ${{ inputs.hostname }}
94
+ # The guarded cf-deploy wrapper resolves SERVER_NAME from the env
95
+ # (set above from the workflow input), runs the placeholder guard,
96
+ # and injects it into wrangler as --var SERVER_NAME:<hostname>.
71
97
  run: pnpm run deploy
72
98
 
73
99
  - name: Resolve smoke URL
package/CHANGELOG.md CHANGED
@@ -10,6 +10,295 @@ For the release process itself — versioning policy, pre-release checklist,
10
10
  cutting a tag, rolling back — see [`docs/Release-Process.md`](docs/Release-Process.md).
11
11
  Cross-reference `progress.md` / `tickets.md` for per-ticket detail.
12
12
 
13
+ ## [0.11.0] - 2026-08-18
14
+
15
+ This is the **security-audit remediation release**. The 2026-08-11
16
+ static audit (`SECURITY-REPORT.md`, 43 findings) is now fully
17
+ remediated: the Critical + first High findings (APIGW body logging,
18
+ OIDC-only deploys) and the CSPRNG id fix shipped in v0.10.0; the
19
+ remaining **40 findings (TICKET-182 … TICKET-222)** land here as a
20
+ layered hardening pass over every edge: **admission control and
21
+ rate limiting at every transport boundary** (global cap, per-IP
22
+ caps, connect-rate windows, per-connection frame budgets, bounded
23
+ line buffers), **credential hardening** (scrypt oper credentials,
24
+ constant-time / timing-equalized comparisons, brute-force lockouts
25
+ on every auth surface), **SASL `EXTERNAL` rebuilt behind an
26
+ operator opt-in with DER-fingerprint account binding**, and a sweep
27
+ of **protocol budget correctness fixes** (UTF-8 byte budgets, tag
28
+ caps, bare-CR rejection).
29
+
30
+ ### Added — Admission control & rate limiting (`irc-core`, `cf-adapter`, `aws-adapter`, `aws-stack`)
31
+
32
+ Layered throttling of connection establishment and frame intake,
33
+ enforced at every transport edge (WS upgrade, NLB stream, TCP
34
+ origin) with `429` rejections.
35
+
36
+ - **Global `MAX_CLIENTS` cap with live-connection counter.**
37
+ Cloudflare reserves a slot in a `CounterDO` counter before
38
+ forwarding an upgrade and answers `429` (`ERROR :Closing link:
39
+ server full`) once the cap is reached; slots are released on
40
+ close and TTL-reaped if a DO dies without one. AWS keeps the
41
+ O(1) atomic counter from v0.8.0, now enforced against the
42
+ configured cap.
43
+ - **Per-IP connection caps + sliding-window connect-rate limits.**
44
+ `MAX_CONNECTIONS_PER_IP` (default 10) caps simultaneous
45
+ connections per source IP; `PER_IP_CONNECTION_RATE_MAX` /
46
+ `PER_IP_CONNECTION_RATE_WINDOW_MS` (default 5 / 60 s) cap new
47
+ establishments per window. Cloudflare enforces both at the worker
48
+ edge via a new `RateLimitDO` (sliding-window admission timestamps
49
+ keyed on `CF-Connecting-IP`; rejections never consume budget, so
50
+ a blocked IP recovers after window decay). AWS `$connect` counts
51
+ live + in-window rows via a new `sourceIp+connectedSince` GSI on
52
+ `Connections`, the NLB path applies the same budget to new flows
53
+ (first chunk only, so active sessions are not starved), API
54
+ Gateway stage throttling (rate + burst parameters) backstops
55
+ globally, and an **opt-in WAFv2 per-IP rate rule**
56
+ (`-c wafConnectRateLimit=<n>`) sits at the edge.
57
+ - **Per-connection inbound frame-rate window.**
58
+ `MAX_FRAMES_PER_WINDOW` / `FRAME_WINDOW_SECONDS` (default 50 /
59
+ 5 s) enforced at the adapter boundary **before** the actor or any
60
+ storage write — a cheap early rejection for frame floods.
61
+ - **Bounded line-buffer memory.** TCP input buffers are capped at
62
+ **8 KiB** at every transport edge (local CLI, container origin,
63
+ NLB handler, forwarder), and the NLB handler rejects oversized
64
+ payloads outright — an unauthenticated peer can no longer pin
65
+ memory with an unterminated byte stream.
66
+
67
+ ### Added — Cloudflare TCP origin hardening (`cf-tcp-container`)
68
+
69
+ The Spectrum origin is rebuilt as a hardened TLS terminator.
70
+
71
+ - **TLS terminates at the origin.** Spectrum switches to
72
+ `tls_mode = "full"` (encrypted all the way to the container, no
73
+ plaintext hop) and the container itself terminates TLS, pinned to
74
+ **TLS 1.2/1.3**.
75
+ - **PROXY protocol v1 is required on every flow.** The container
76
+ parses the `PROXY TCP4/TCP6 …` header (incremental, 107-byte
77
+ budget) to recover the real client IP — and **rejects flows
78
+ without one** — so per-IP admission (above) cannot be spoofed by
79
+ connecting directly to the origin. Origin ingress is locked to
80
+ Spectrum's published IP ranges in the Terraform.
81
+ - **Non-root runtime.** The container runs as the non-root `node`
82
+ user (UID 1000); the Dockerfile drops to an unprivileged user and
83
+ the cert files are owned by it.
84
+
85
+ ### Added — AWS stack hardening (`aws-stack`, `aws-adapter`)
86
+
87
+ - **State tables protected.** Every DynamoDB state table
88
+ (`Connections`, `Nicks`, `Channels`, `Services`, …) now carries
89
+ `RETAIN` deletion policy, **deletion protection**, and
90
+ **point-in-time recovery (PITR)** — a stray `cdk destroy` or a
91
+ compromised console session can no longer silently destroy
92
+ persistent IRC state.
93
+ - **NLB stream path integrity.** The streaming handler asserts the
94
+ flow is TLS-secured (surfacing user mode `S`), **rejects flows
95
+ with missing or malformed `x-forwarded-for` / `x-forwarded-port`
96
+ headers**, and its Lambda now holds the
97
+ `execute-api:ManageConnections` grant against the management API
98
+ stage (fixes silent fanout failure on the TCP path).
99
+ - **APIGW logging role least-privilege.** The CloudWatch logging
100
+ role's policy is scoped to exact write-only grants on the
101
+ API-specific log groups.
102
+
103
+ ### Added — Credential hardening (`irc-core`)
104
+
105
+ - **Scrypt-hashed oper credentials.** `OPER` verifies against a
106
+ scrypt hash + salt (`OPER_SALT` + `OPER_HASH`) with a timing-safe
107
+ compare; a new `tools/hash-oper-cred.ts` generates the pair
108
+ (`node --import tsx tools/hash-oper-cred.ts --user admin --stdin`).
109
+ The plaintext `OPER_PASSWORD` form still works but logs a
110
+ deprecation warning and will be removed.
111
+ - **Constant-time server-password comparison.** The
112
+ `SERVER_PASSWORD` gate compares in constant time.
113
+ - **Timing-equalized nick verification (no account enumeration).**
114
+ `verifyNick` runs a dummy scrypt verify for unknown accounts, so
115
+ response timing cannot distinguish "no such nick" from "wrong
116
+ password". NickServ `INFO` / `SET PASSWORD` replies are already
117
+ indistinguishable on the same axis.
118
+ - **Brute-force lockouts on every auth surface.** A per-connection
119
+ **SASL failure lockout** (repeated `904`s disconnect with "too
120
+ many failed SASL attempts"), a **per-IP `OPER` failure lockout**
121
+ (with oper-broadcast on trip), and a **per-account NickServ
122
+ `IDENTIFY` freeze**. The `AUTHENTICATE` **flood-control exemption
123
+ is removed** — SASL frames now count against the connection's
124
+ message bucket, closing the timing-oracle bandwidth amplifier.
125
+ - **Credential redaction from parse-error logs.** Parse-error log
126
+ records carry only a truncated leading token, the length, and a
127
+ stable reason code — never the raw line (a malformed line could
128
+ otherwise smuggle `PASS <secret>` past the parser into logs).
129
+ Workers log sampling is lowered to **10%** by default.
130
+
131
+ ### Changed — SASL `EXTERNAL` (mTLS) rebuilt behind an operator opt-in (`irc-core`, adapters)
132
+
133
+ EXTERNAL is now advertised and accepted only when **all three**
134
+ hold: a bound mTLS identity source (CF API Shield / APIGW client
135
+ certs), the operator opt-in **`EXTERNAL_ENABLED`** (`true`/`1`,
136
+ **default off** — surfaced via both adapters' config loaders), and
137
+ a secure (TLS) connection. The `sasl` cap value and the
138
+ `AUTHENTICATE` reducer share the same gate so advertisement and
139
+ enforcement cannot drift; refusals answer `908 ERR_SASLMECHS`
140
+ listing `PLAIN` only and do not count toward the SASL lockout.
141
+
142
+ - **Accounts bind to the certificate's DER SHA-256 fingerprint**
143
+ (`fp:<hex>` entries in `RegisteredNick.certSubjects` — what
144
+ Cloudflare surfaces from `request.cf.tlsClientAuth`; the preferred
145
+ binding) **or a canonical subject DN** (the only identifier API
146
+ Gateway exposes). DNs are canonicalised before comparison —
147
+ attribute types lower-cased, whitespace collapsed, RDN/AVA
148
+ components sorted — so semantically equal DNs written differently
149
+ (including across cert re-issuance) still match. Legacy verbatim
150
+ bindings verify and are rewritten to the canonical spelling on
151
+ first use.
152
+ - **Transports without a client-cert surface pin the mechanism off.**
153
+ The `cf-tcp-container` Spectrum origin (and the NLB stream handler
154
+ unless an mTLS provider is injected) reject
155
+ `AUTHENTICATE EXTERNAL` outright with a transport-specific
156
+ `904 ERR_SASLFAIL` instead of silently mis-binding.
157
+
158
+ ### Changed — Protocol budget correctness (`irc-core`, `irc-server`)
159
+
160
+ - **All length budgets are enforced in UTF-8 bytes, not UTF-16 code
161
+ units** — the 512-byte line cap, `TOPICLEN` (truncated on
162
+ character boundaries, never mid-multibyte), `NICKLEN` /
163
+ `CHANNELLEN`, and the `draft/multiline` batch budget, which is
164
+ now also **enforced incrementally as lines arrive** (an
165
+ over-budget batch aborts mid-stream with `FAIL`, not just at
166
+ close).
167
+ - **IRCv3 tag limits enforced in the parser**: at most **15 tags**
168
+ and an **8191-byte tag section** per message.
169
+ - **Multi-target commands capped.** Comma-split
170
+ `PRIVMSG`/`NOTICE`/`TAGMSG`/`PART` targets beyond
171
+ `MAX_TARGETS_PER_COMMAND` are dropped with `407 ERR_TOOMANYTARGETS`.
172
+ - **Long `353 NAMES` replies split** across multiple lines within
173
+ the 510-byte budget (previously one over-long line).
174
+ - **`CHATHISTORY` limits capped** at a configurable ceiling
175
+ (default 100) — `CHATHISTORY LATEST #chan * 999999999` no longer
176
+ asks for an unbounded replay.
177
+ - **Bare-CR smuggling rejected.** Any CR remaining after the
178
+ trailing CRLF strip is a parse error (`421`), and outbound
179
+ `RawLine.text` is sanitized of stray CR.
180
+ - **Channel names validated in one shared module** — NUL and
181
+ control characters are forbidden outright.
182
+ - **`USER` username/realname validated** (charset, byte length, no
183
+ control characters) before `001`.
184
+ - **`OPER` requires a registered connection** (`451
185
+ ERR_NOTREGISTERED` pre-registration).
186
+ - **`parseTags` builds a null-prototype map** — a `__proto__` tag
187
+ can no longer pollute the tags object (prototype pollution).
188
+
189
+ ### Changed — Services hardening (`irc-core`)
190
+
191
+ - **ChanServ privilege guards.** `DEOP`/`KICK` cannot target the
192
+ founder from a non-founder, and roster-mutation commands check the
193
+ caller's access rank (`SOP`/`AOP`/`HOP`/`VOP`) before acting.
194
+ - **HostServ auto-approve denylist/allowlist.** When vhost
195
+ auto-approve is enabled, requests are checked against a
196
+ configurable denylist (and optional allowlist) instead of
197
+ approving everything.
198
+
199
+ ### Added — CI / deploy guards (`ci-hardening`, deploy workflows)
200
+
201
+ - **Env-var documentation drift guard.** The
202
+ `consumed-env-vars` block in `apps/cf-worker/wrangler.toml` is
203
+ contract-checked against the cf-adapter config loader source on
204
+ every CI run: every consumed var must be documented (classified
205
+ `[vars]` vs `[secret]`), no stale names, and no `[secret]`-class
206
+ var may ship as a plaintext `[vars]` entry.
207
+ - **Placeholder-hostname deploy rejection.** The CF deploy workflow
208
+ refuses to ship while `SERVER_NAME` is still a repo placeholder
209
+ (`irc.example.com` / `irc.your-domain.invalid`).
210
+
211
+ ### Changed — Documentation
212
+
213
+ - **`README.md` rewritten to current capabilities**: the abuse
214
+ controls & credential hardening surface (admission/rate table,
215
+ protocol budgets, lockouts, the SASL EXTERNAL triple gate), the
216
+ hardened TCP origins (PROXY v1 + origin TLS on CF, NLB assertions
217
+ on AWS), the new tools, accurate coverage state, and an updated
218
+ roadmap.
219
+
220
+ ### ⚠️ Migration required
221
+
222
+ - **No persisted-DO-state schema change.** `PERSISTED_STATE_VERSION`
223
+ stays `1`; the D1/DynamoDB services tables are unchanged (the
224
+ canonical-DN rewrite happens lazily at compare time and is
225
+ backward-compatible). **Code rollback across this release is
226
+ safe.**
227
+ - **SASL `EXTERNAL` is now OFF by default.** If you rely on mTLS
228
+ EXTERNAL, set `EXTERNAL_ENABLED=true` (Workers `[vars]` / Lambda
229
+ env) **and** ensure edge mTLS (API Shield / APIGW client certs)
230
+ is configured — the flag alone advertises nothing. Accounts
231
+ should re-bind to the certificate's DER fingerprint
232
+ (`fp:<hex>`) where the platform surfaces it.
233
+ - **CF TCP origin: reconfigure Spectrum before deploying.** The
234
+ container now terminates TLS and **requires PROXY protocol v1**
235
+ on every flow. Update the Spectrum app to
236
+ `proxy_protocol = "v1"` + `tls_mode = "full"`, mount the origin
237
+ certificate readable by UID 1000, and keep origin ingress locked
238
+ to Spectrum's IP ranges. See `docs/Cloudflare-TCP-Deployment.md`.
239
+ - **AWS: NLB target must preserve flow headers.** The stream
240
+ handler now rejects flows missing `x-forwarded-for` /
241
+ `x-forwarded-port` (an NLB target-group attribute) — a
242
+ misconfigured LB now fails loudly instead of mis-attributing IPs.
243
+ - **Admission defaults are now ON.** `MAX_CONNECTIONS_PER_IP=10`,
244
+ per-user 5, connect-rate 5/60 s, and 50 frames / 5 s apply even
245
+ when unset. Deployments behind a shared NAT / proxy (offices,
246
+ bridges, health probes) should raise `MAX_CONNECTIONS_PER_IP` /
247
+ the rate budget to taste.
248
+ - **Oper credentials: migrate off plaintext.** `OPER_PASSWORD`
249
+ still works but is deprecated. Generate
250
+ `OPER_SALT`/`OPER_HASH` with `tools/hash-oper-cred.ts` and set
251
+ them as Workers secrets / Lambda env.
252
+ - **Client-visible behaviour changes.** (1) `AUTHENTICATE` frames
253
+ now count against flood control (a client that sent SASL at line
254
+ rate will be disconnected as an excess flood). (2) An inbound
255
+ frame with an embedded bare CR is now a parse error answered with
256
+ `421` (previously in legacy mode it could smuggle content).
257
+ (3) Multi-target commands beyond the cap get `407` for the
258
+ dropped targets. No compliant client is known to trip any of
259
+ these.
260
+
261
+ ### Known limitations
262
+
263
+ - **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
264
+ TheLounge / matrix-IRC bridge) remains pending.
265
+ - **Web client e2e** (Playwright headless-browser vs a deployed
266
+ stack) remains pending; the AWS staging frontend e2e
267
+ (TICKET-169) is still open.
268
+ - Same transport limits as v0.10.0: the **CF TCP path requires
269
+ Cloudflare Spectrum (Enterprise tier)**; the **AWS TCP path** is
270
+ subject to Lambda stream-duration limits. WebSocket remains the
271
+ zero-extra-deps default.
272
+ - **Coverage hardening is near-complete.** `irc-core`,
273
+ `irc-server`, `in-memory-runtime`, `irc-test-support`,
274
+ `cf-worker`, `local-cli`, `aws-stack`, `load-test`,
275
+ `cf-tcp-container`, `tcp-ws-forwarder`, `ci-hardening`, and
276
+ `deploy-scripts` sit at 100% lines; `cf-adapter` (~99.7%) and
277
+ `aws-adapter` (~98%) clear the 90% gate with follow-ups open;
278
+ `web` is at 100% lines / ~98% branches.
279
+
280
+ ### Security
281
+
282
+ This release is itself a security remediation; the headline items:
283
+
284
+ - **Admission + rate limiting at every edge** (above) — the audit's
285
+ "no throttling anywhere" theme: connection floods, per-IP
286
+ takeovers of the connection budget, and frame floods are now
287
+ bounded by default.
288
+ - **Plaintext oper credentials replaced** with scrypt hashes +
289
+ timing-safe verify (above).
290
+ - **Account-enumeration oracles closed**: timing-equalized
291
+ `verifyNick`, constant-time server-password compare (above).
292
+ - **Brute-force lockouts** on SASL / OPER / NickServ `IDENTIFY`
293
+ (above).
294
+ - **SASL `EXTERNAL` safe by default** (off unless the operator
295
+ opts in; fingerprint-based binding) and **bare-CR / prototype-
296
+ pollution / oversized-payload parser hardening** (above).
297
+ - **Credential redaction from logs** + 10% default sampling
298
+ (above), complementing v0.10.0's `DataTraceEnabled` hard-lock.
299
+
300
+ ---
301
+
13
302
  ## [0.10.0] - 2026-08-13
14
303
 
15
304
  The main change in this release is **AWS web client parity** — the