serverless-ircd 0.9.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 (254) hide show
  1. package/.github/workflows/ci.yml +28 -0
  2. package/.github/workflows/deploy-aws.yml +156 -32
  3. package/.github/workflows/deploy-cf-tcp.yml +35 -9
  4. package/.github/workflows/deploy-cf.yml +40 -14
  5. package/CHANGELOG.md +594 -0
  6. package/README.md +286 -60
  7. package/apps/aws-stack/README.md +3 -5
  8. package/apps/aws-stack/bin/aws.ts +118 -9
  9. package/apps/aws-stack/cdk.json +0 -3
  10. package/apps/aws-stack/package.json +3 -4
  11. package/apps/aws-stack/src/aws-stack.ts +398 -67
  12. package/apps/aws-stack/src/static-site.ts +323 -0
  13. package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
  14. package/apps/aws-stack/tests/stack.test.ts +714 -105
  15. package/apps/aws-stack/tests/static-site.test.ts +491 -0
  16. package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
  17. package/apps/cf-tcp-container/Dockerfile +37 -5
  18. package/apps/cf-tcp-container/package.json +7 -3
  19. package/apps/cf-tcp-container/src/config-loader.ts +113 -2
  20. package/apps/cf-tcp-container/src/container-server.ts +267 -87
  21. package/apps/cf-tcp-container/src/main.ts +22 -7
  22. package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
  25. package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
  26. package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
  27. package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
  28. package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
  29. package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
  30. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
  31. package/apps/cf-tcp-container/wrangler.toml +18 -14
  32. package/apps/cf-worker/package.json +3 -4
  33. package/apps/cf-worker/src/worker.ts +77 -5
  34. package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
  35. package/apps/cf-worker/tests/smoke.test.ts +4 -0
  36. package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
  37. package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
  38. package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
  39. package/apps/cf-worker/wrangler.test.toml +15 -1
  40. package/apps/cf-worker/wrangler.toml +95 -77
  41. package/apps/local-cli/package.json +1 -1
  42. package/apps/local-cli/src/config-loader.ts +14 -2
  43. package/apps/local-cli/src/line-scanner.ts +26 -0
  44. package/apps/local-cli/src/server.ts +44 -19
  45. package/apps/local-cli/tests/line-scanner.test.ts +64 -0
  46. package/apps/local-cli/tests/tcp.test.ts +29 -0
  47. package/apps/web/landing/favicon.ico +0 -0
  48. package/apps/web/landing/index.html +1 -0
  49. package/apps/web/package.json +2 -2
  50. package/apps/web/scripts/build.mjs +66 -4
  51. package/apps/web/src/build-env.ts +125 -4
  52. package/apps/web/src/config-schema.ts +20 -6
  53. package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
  54. package/apps/web/tests/build-env.test.ts +210 -9
  55. package/apps/web/tests/build-smoke.test.ts +2 -2
  56. package/apps/web/tests/config-schema.test.ts +149 -25
  57. package/docs/AWS-Deployment.md +793 -118
  58. package/docs/AWS-TCP-Deployment.md +57 -47
  59. package/docs/Chat-History.md +55 -0
  60. package/docs/Cloudflare-Deployment-Guide.md +95 -114
  61. package/docs/Cloudflare-TCP-Deployment.md +160 -101
  62. package/docs/Release-Process.md +27 -23
  63. package/docs/SASL-EXTERNAL.md +175 -0
  64. package/docs/Services.md +69 -22
  65. package/docs/WebClientGuide.md +35 -26
  66. package/package.json +7 -10
  67. package/packages/aws-adapter/package.json +1 -1
  68. package/packages/aws-adapter/src/admission.ts +28 -13
  69. package/packages/aws-adapter/src/aws-runtime.ts +30 -3
  70. package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
  71. package/packages/aws-adapter/src/config-loader.ts +153 -8
  72. package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
  73. package/packages/aws-adapter/src/handlers/connect.ts +73 -1
  74. package/packages/aws-adapter/src/handlers/default.ts +279 -123
  75. package/packages/aws-adapter/src/handlers/index.ts +98 -25
  76. package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
  77. package/packages/aws-adapter/src/index.ts +5 -7
  78. package/packages/aws-adapter/src/ip-admission.ts +79 -0
  79. package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
  80. package/packages/aws-adapter/src/serialize.ts +23 -0
  81. package/packages/aws-adapter/src/tables.ts +11 -12
  82. package/packages/aws-adapter/tests/admission.test.ts +60 -2
  83. package/packages/aws-adapter/tests/aws-harness.ts +23 -2
  84. package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
  85. package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
  86. package/packages/aws-adapter/tests/connect.test.ts +323 -3
  87. package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
  88. package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
  89. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
  90. package/packages/aws-adapter/tests/handlers.test.ts +174 -12
  91. package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
  92. package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
  93. package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
  94. package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
  95. package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
  96. package/packages/aws-adapter/tests/stats.test.ts +0 -3
  97. package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
  98. package/packages/aws-adapter/tests/tables.test.ts +1 -8
  99. package/packages/aws-adapter/tests/transactions.test.ts +0 -1
  100. package/packages/cf-adapter/package.json +1 -5
  101. package/packages/cf-adapter/src/cf-runtime.ts +100 -10
  102. package/packages/cf-adapter/src/channel-do.ts +13 -3
  103. package/packages/cf-adapter/src/config-loader.ts +133 -8
  104. package/packages/cf-adapter/src/connection-do.ts +406 -116
  105. package/packages/cf-adapter/src/counter-do.ts +142 -0
  106. package/packages/cf-adapter/src/d1-services-store.ts +105 -26
  107. package/packages/cf-adapter/src/env.ts +99 -10
  108. package/packages/cf-adapter/src/index.ts +17 -7
  109. package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
  110. package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
  111. package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
  112. package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
  113. package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
  114. package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
  115. package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
  116. package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
  117. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
  118. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
  119. package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
  120. package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
  121. package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
  122. package/packages/cf-adapter/tests/serialize.test.ts +25 -0
  123. package/packages/cf-adapter/tests/worker/main.ts +4 -0
  124. package/packages/cf-adapter/wrangler.test.toml +18 -1
  125. package/packages/in-memory-runtime/package.json +1 -1
  126. package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
  127. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
  128. package/packages/irc-core/package.json +1 -1
  129. package/packages/irc-core/src/account-migration.ts +140 -0
  130. package/packages/irc-core/src/caps/capabilities.ts +20 -10
  131. package/packages/irc-core/src/certfp.ts +178 -0
  132. package/packages/irc-core/src/commands/account-auth.ts +16 -19
  133. package/packages/irc-core/src/commands/cap.ts +10 -2
  134. package/packages/irc-core/src/commands/chanserv.ts +117 -14
  135. package/packages/irc-core/src/commands/chathistory.ts +13 -5
  136. package/packages/irc-core/src/commands/hostserv.ts +84 -8
  137. package/packages/irc-core/src/commands/index.ts +2 -1
  138. package/packages/irc-core/src/commands/invite.ts +1 -7
  139. package/packages/irc-core/src/commands/join.ts +1 -16
  140. package/packages/irc-core/src/commands/kick.ts +1 -8
  141. package/packages/irc-core/src/commands/list.ts +1 -8
  142. package/packages/irc-core/src/commands/memoserv.ts +1 -1
  143. package/packages/irc-core/src/commands/mode.ts +1 -8
  144. package/packages/irc-core/src/commands/multiline.ts +4 -10
  145. package/packages/irc-core/src/commands/names.ts +53 -13
  146. package/packages/irc-core/src/commands/nickserv.ts +161 -11
  147. package/packages/irc-core/src/commands/oper.ts +361 -8
  148. package/packages/irc-core/src/commands/part.ts +4 -10
  149. package/packages/irc-core/src/commands/privmsg.ts +8 -4
  150. package/packages/irc-core/src/commands/registration.ts +148 -4
  151. package/packages/irc-core/src/commands/sasl.ts +154 -46
  152. package/packages/irc-core/src/commands/topic.ts +10 -12
  153. package/packages/irc-core/src/commands/who.ts +1 -8
  154. package/packages/irc-core/src/config.ts +424 -25
  155. package/packages/irc-core/src/credential-hashing.ts +11 -54
  156. package/packages/irc-core/src/effects.ts +24 -0
  157. package/packages/irc-core/src/flood-control.ts +10 -10
  158. package/packages/irc-core/src/frame-rate-limit.ts +82 -0
  159. package/packages/irc-core/src/index.ts +9 -0
  160. package/packages/irc-core/src/oper-hashing.ts +43 -0
  161. package/packages/irc-core/src/oper-lockout.ts +87 -0
  162. package/packages/irc-core/src/ports.ts +529 -190
  163. package/packages/irc-core/src/protocol/bytes.ts +65 -0
  164. package/packages/irc-core/src/protocol/channel-name.ts +37 -0
  165. package/packages/irc-core/src/protocol/index.ts +12 -1
  166. package/packages/irc-core/src/protocol/outbound.ts +43 -10
  167. package/packages/irc-core/src/protocol/parser.ts +79 -10
  168. package/packages/irc-core/src/state/connection.ts +13 -0
  169. package/packages/irc-core/src/types.ts +266 -23
  170. package/packages/irc-core/src/ws-framing.ts +5 -4
  171. package/packages/irc-core/tests/account-migration.test.ts +133 -0
  172. package/packages/irc-core/tests/bytes.test.ts +89 -0
  173. package/packages/irc-core/tests/certfp.test.ts +117 -0
  174. package/packages/irc-core/tests/commands/cap.test.ts +76 -2
  175. package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
  176. package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
  177. package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
  178. package/packages/irc-core/tests/commands/join.test.ts +78 -1
  179. package/packages/irc-core/tests/commands/markread.test.ts +54 -0
  180. package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
  181. package/packages/irc-core/tests/commands/names.test.ts +193 -0
  182. package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
  183. package/packages/irc-core/tests/commands/oper.test.ts +574 -1
  184. package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
  185. package/packages/irc-core/tests/commands/registration.test.ts +602 -133
  186. package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
  187. package/packages/irc-core/tests/commands/topic.test.ts +137 -2
  188. package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
  189. package/packages/irc-core/tests/config.test.ts +534 -2
  190. package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
  191. package/packages/irc-core/tests/effects.test.ts +14 -0
  192. package/packages/irc-core/tests/flood-control.test.ts +29 -1
  193. package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
  194. package/packages/irc-core/tests/message-store.test.ts +5 -0
  195. package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
  196. package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
  197. package/packages/irc-core/tests/outbound.test.ts +148 -0
  198. package/packages/irc-core/tests/parser.test.ts +287 -5
  199. package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
  200. package/packages/irc-core/tests/ports.test.ts +170 -7
  201. package/packages/irc-core/tests/services-store.test.ts +567 -1
  202. package/packages/irc-core/tests/ws-framing.test.ts +45 -0
  203. package/packages/irc-core/vitest.config.ts +6 -1
  204. package/packages/irc-server/package.json +1 -1
  205. package/packages/irc-server/src/actor.ts +123 -22
  206. package/packages/irc-server/src/dispatch.ts +1 -0
  207. package/packages/irc-server/src/index.ts +7 -0
  208. package/packages/irc-server/src/redact.ts +159 -0
  209. package/packages/irc-server/src/runtime.ts +14 -0
  210. package/packages/irc-server/src/transport.ts +28 -1
  211. package/packages/irc-server/tests/actor.test.ts +563 -54
  212. package/packages/irc-server/tests/dispatch.test.ts +31 -0
  213. package/packages/irc-server/tests/redact.test.ts +198 -0
  214. package/packages/irc-server/tests/runtime.test.ts +2 -0
  215. package/packages/irc-server/tests/transport.test.ts +66 -0
  216. package/packages/irc-test-support/package.json +1 -1
  217. package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
  218. package/pnpm-workspace.yaml +1 -0
  219. package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
  220. package/scripts/deploy-web-aws.mjs +290 -0
  221. package/scripts/package.json +23 -0
  222. package/scripts/tsconfig.test.json +12 -0
  223. package/scripts/vitest.config.ts +19 -0
  224. package/tools/ci-hardening/package.json +2 -2
  225. package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
  226. package/tools/ci-hardening/src/cf-deploy.ts +118 -0
  227. package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
  228. package/tools/ci-hardening/src/env-var-drift.ts +192 -0
  229. package/tools/ci-hardening/src/hostname-guard.ts +11 -0
  230. package/tools/ci-hardening/src/index.ts +19 -0
  231. package/tools/ci-hardening/src/validate.ts +57 -0
  232. package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
  233. package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
  234. package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
  235. package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
  236. package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
  237. package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
  238. package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
  239. package/tools/ci-hardening/tests/validate.test.ts +42 -0
  240. package/tools/ci-hardening/vitest.config.ts +5 -1
  241. package/tools/hash-oper-cred.ts +85 -0
  242. package/tools/load-test/package.json +1 -1
  243. package/tools/migrate-accounts-to-services.ts +270 -0
  244. package/tools/package.json +2 -1
  245. package/tools/seed-aws-accounts.ts +35 -10
  246. package/tools/seed-cf-accounts.ts +42 -9
  247. package/tools/tcp-ws-forwarder/package.json +1 -1
  248. package/packages/aws-adapter/src/account-store.ts +0 -121
  249. package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
  250. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
  251. package/packages/aws-adapter/tests/account-store.test.ts +0 -276
  252. package/packages/cf-adapter/src/d1-account-store.ts +0 -198
  253. package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
  254. package/packages/irc-core/tests/account-store.test.ts +0 -131
package/CHANGELOG.md CHANGED
@@ -10,6 +10,600 @@ 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
+
302
+ ## [0.10.0] - 2026-08-13
303
+
304
+ The main change in this release is **AWS web client parity** — the
305
+ vendored Kiwi IRC SPA is no longer Cloudflare-only. AWS now hosts it
306
+ via an **S3 + CloudFront + OAC** `StaticSite` construct (opt-in via
307
+ CDK context) with a **stack-output-driven two-phase deploy**, and the
308
+ API Gateway `$connect` route gains an opt-in `WEB_ORIGINS` CSWSH
309
+ defence that mirrors the CF Worker's. Alongside it: the SASL
310
+ `AccountStore` is **collapsed into `ServicesStore`** as the single
311
+ credential home (a one-shot backfill migration ships for existing
312
+ deployments), **NickServ `SET PASSWORD`** lands as self-service
313
+ password change, and the staging/prod deploy split is **collapsed**
314
+ into a single target per platform. A round of **AWS deploy security
315
+ hardening** ships: the CI workflow is **OIDC-only** (no long-lived
316
+ access keys), and API Gateway full-frame body logging
317
+ (`DataTraceEnabled`) is **off by default and hard-locked** so IRC
318
+ frames are never written to CloudWatch. Finally, `UuidIdFactory` now
319
+ sources its random bytes from `globalThis.crypto` (CSPRNG) instead of
320
+ `Math.random`, so generated `msgid` / session ids are cryptographic-
321
+ grade.
322
+
323
+ ### Added — AWS web client (`@serverless-ircd/aws-stack`, `@serverless-ircd/aws-adapter`, `@serverless-ircd/web`)
324
+
325
+ The web client is no longer Cloudflare-only. AWS provisions a
326
+ static-site origin for the SPA while the wss IRC endpoint stays on
327
+ API Gateway — two origins, same `apps/web/dist/` artifact.
328
+
329
+ - **`StaticSite` construct** (`apps/aws-stack/src/static-site.ts`): a
330
+ private S3 origin bucket fronted by a CloudFront distribution with
331
+ **Origin Access Control (OAC)** — direct S3 access is denied by the
332
+ bucket policy; only CloudFront URLs resolve. `defaultRootObject` is
333
+ `index.html` (the project landing page at `/`), and a custom error
334
+ response maps S3 `403`/`404` → `200 /webclient/index.html` so
335
+ client-side SPA routes resolve. Custom domain (ACM certificate +
336
+ Route53 `ARecord` alias) is opt-in via CDK context
337
+ (`webSiteCustomDomain` / `webSiteCertificateArn` /
338
+ `webSiteHostedZoneName` / `webSiteHostedZoneId`); omitted by default
339
+ to use `*.cloudfront.net`. Three stable CfnOutputs (`WebsiteURL`,
340
+ `WebsiteBucketName`, `WebsiteDistributionId`) drive the deploy
341
+ pipeline below.
342
+ - **CloudFront directory-index viewer-request function**: S3 REST
343
+ (used with OAC) does not serve index documents, so `/docs/` or
344
+ `/docs` would 404 and fall through to the SPA error response
345
+ (serving the Kiwi shell instead of the rendered docs index). A
346
+ viewer-request CloudFront function now resolves directory-style
347
+ URIs: trailing-slash paths (`/docs/`, `/`) get `index.html`
348
+ appended as an internal rewrite, and extensionless paths without a
349
+ trailing slash (`/docs`) get a `301` redirect to the trailing-slash
350
+ form so the browser resolves relative asset links against the
351
+ directory. Real assets (`.html`, `.css`, `.js`, …) are served
352
+ unchanged. Mirrors the CF Worker `[assets]` binding, which resolves
353
+ directory indexes automatically.
354
+ - **`$connect` Origin allowlist (CSWSH defence)**: API Gateway
355
+ WebSocket has no built-in `Origin` validation, so the `$connect`
356
+ Lambda now enforces it. `WEB_ORIGINS` (comma-separated) is parsed
357
+ into a normalised lowercase set at cold start; a browser-sent
358
+ `Origin` not in the set is denied with `403` (API Gateway closes
359
+ the upgrade). The defence is **opt-in** — unset `WEB_ORIGINS`
360
+ disables the check entirely so existing bare-IRC deployments without
361
+ a web frontend are unchanged on upgrade. Non-browser clients (curl,
362
+ WeeChat, the `tcp-ws-forwarder`, the NLB TCP+TLS path) never send
363
+ `Origin` and always proceed. Deliberate divergence from the CF
364
+ policy: **explicit-allowlist only** (no same-origin auto-derive),
365
+ because the SPA is on a CloudFront origin and the wss endpoint is
366
+ on an API Gateway origin, so the request's own host is never the
367
+ SPA's origin. Mirrors the CF Worker's parsing semantics (trimmed,
368
+ lowercased, scheme+host match).
369
+ - **`prod-aws` build env**: a new `apps/web/static/config.prod-aws.json`
370
+ + `--env prod-aws` (and a `build:prod-aws` pnpm script) select the
371
+ AWS API-Gateway-shaped config. The `server` field carries
372
+ `{{API_ID}}` / `{{REGION}}` / `{{STAGE}}` placeholders the deploy
373
+ pipeline substitutes at bake time, so no concrete wss URL is
374
+ hardcoded in the repo. Because irc-framework prepends `wss://`
375
+ itself from `tls: true`, the baked URL is **split** into a
376
+ hostname-only `server` and a stage-name `direct_path` (e.g.
377
+ `/<stage>`) — baking the full `wss://` URL into `server` produced
378
+ a doubled-scheme `wss://wss//...` URL that Firefox rejects.
379
+ - **Stack-output-driven deploy pipeline** (`scripts/deploy-web-aws.mjs`,
380
+ a new `@serverless-ircd/deploy-scripts` workspace package): unlike
381
+ the CF Worker (where `wrangler deploy` ships the SPA in the same
382
+ command as the runtime), the AWS SPA deploy is **two-phase** because
383
+ the baked `config.json` carries the API Gateway WebSocket URL as a
384
+ literal that does not exist at synth time. The script runs
385
+ `describe-stacks` → `pnpm --filter web build:prod-aws -- --api-url
386
+ <ConnectUrl>` → `aws s3 sync --delete` →
387
+ `cloudfront create-invalidation /*`. Fails loudly on missing stack
388
+ outputs or unsubstituted `{{...}}` placeholders. The
389
+ `deploy-web` CI job (`.github/workflows/deploy-aws.yml`) runs it
390
+ with `needs: deploy`.
391
+
392
+ ### Added — NickServ `SET PASSWORD` (`@serverless-ircd/irc-core`)
393
+
394
+ - **Self-service password change.** `PRIVMSG NickServ :SET PASSWORD
395
+ <old> <new>` re-verifies the current password (defence against a
396
+ hijacked `+r` session), enforces a configurable minimum (default 8)
397
+ and a fixed maximum (256, the scrypt input budget), then re-hashes
398
+ and persists via `ServicesStore.setNickPassword`. The caller's
399
+ session stays `+r`; other sessions on the same account are not
400
+ kicked. Wrong old password is indistinguishable from "no such
401
+ account" (no enumeration vector). Wired through
402
+ `InMemoryServicesStore` and `PersistentServicesStore` with round-
403
+ trip tests on the D1 and DynamoDB backends.
404
+
405
+ ### Added — CSPRNG-backed ids (`@serverless-ircd/irc-core`)
406
+
407
+ - **`UuidIdFactory` now uses `globalThis.crypto.getRandomValues`.** The
408
+ previous `Math.random`-based UUIDv4 generation was unsuitable for
409
+ unforgeable `msgid` / session ids — a predictable id would let a
410
+ client forge message attribution. The global `crypto.getRandomValues`
411
+ is available on Node ≥ 19 and Cloudflare Workers without an import.
412
+ Generated ids are now cryptographic-grade. The `IdFactory` port and
413
+ the deterministic `testIdFactory` test seam are unchanged, so
414
+ reducer determinism (reducers never touch real randomness) is
415
+ preserved; only the production factory changed.
416
+
417
+ ### Changed — Single credential home (`@serverless-ircd/irc-core`, `@serverless-ircd/cf-adapter`, `@serverless-ircd/aws-adapter`)
418
+
419
+ The SASL `AccountStore` port is **removed**. `ServicesStore` is now
420
+ the single credential home: SASL PLAIN, SASL EXTERNAL (CertFP),
421
+ NickServ `IDENTIFY`, and `PASS <nick>:<password>` all verify through
422
+ the same scrypt-hashed `verifyNick` / `verifyCertFP` surface. This
423
+ closes the cross-store credential drift flagged as a known limitation
424
+ in v0.9.0 — a nick registered via NickServ `REGISTER` after the worker
425
+ booted now authenticates via SASL/PASS immediately, because there is
426
+ no second store to drift out of sync with.
427
+
428
+ - **`ServicesStore` grows CertFP**: `verifyCertFP` / `addCertFP` /
429
+ `removeCertFP` are added to the port, and `RegisteredNick` gains a
430
+ `certSubjects: string[]` field so SASL EXTERNAL resolves through the
431
+ services store (previously SASL EXTERNAL only worked against the
432
+ static `InMemoryAccountStore`, never the hashed D1/DynamoDB table).
433
+ - **D1 `cert_subjects` column auto-migrates**: the CF D1 path runs
434
+ `ALTER TABLE nickserv_accounts ADD COLUMN cert_subjects` inside
435
+ `migrateServicesSchema` on cold start (`CREATE TABLE IF NOT EXISTS`
436
+ + the `ALTER`), so no manual DDL is needed. DynamoDB is schemaless.
437
+ - **Removed**: `AccountStore` port, `InMemoryAccountStore`,
438
+ `HashedAccountStore`, the D1/DynamoDB `account-store`
439
+ implementations, `SaslPayload` / `SaslResult` / `SaslAccountCredential`
440
+ / `constantTimeEquals` / `ingestAccountCredential`, and the
441
+ `accounts` / `Accounts` table reads. The new code only reads
442
+ `nickserv_accounts` / `Services`.
443
+ - **`tools/migrate-accounts-to-services.ts`**: a one-shot, idempotent,
444
+ never-overwrite backfill of legacy `accounts` / `Accounts` rows into
445
+ `nickserv_accounts` / `Services`. Reads the still-deployed
446
+ `SASL_ACCOUNTS` env var at run time so env-seeded accounts are
447
+ carried into the services table. `cert_subjects` defaults to `'[]'`
448
+ for every migrated row (the legacy table had no cert column).
449
+ **Run this before the v0.10.0 build deploys** — see the migration
450
+ section below.
451
+
452
+ ### Changed — Single deploy target per platform
453
+
454
+ The staging/prod deploy split is **collapsed** into one target per
455
+ platform. Staging vs production isolation is now driven by which
456
+ account/region (AWS) or which Worker credentials (CF) the deploy
457
+ targets, not by stack/table/Worker name templating.
458
+
459
+ - **AWS CDK**: drops the `environmentName` prop, `prefixedTableName`,
460
+ and the `IrcAwsStack-<env>` stack id. Tables use bare logical ids
461
+ (`Connections`, `Nicks`, …); the stack is always `IrcAwsStack`.
462
+ - **Cloudflare**: drops the `env.staging` blocks in the cf-worker and
463
+ cf-tcp-container wrangler configs.
464
+ - **Web build**: drops `build:staging` / `config.staging.json`; `--env`
465
+ now accepts only `prod` (or the default dev config) and `prod-aws`.
466
+ - **CI**: collapses `deploy-*: staging` jobs into single `deploy` /
467
+ `deploy-web` jobs; script names align (`pnpm deploy:cf`,
468
+ `pnpm deploy:aws`, `pnpm deploy:cf:tcp`, `pnpm smoke:aws`,
469
+ `pnpm smoke:cf`). Deploy workflows remain `workflow_dispatch`-only
470
+ (no push trigger).
471
+
472
+ ### Added — AWS deploy security
473
+
474
+ - **OIDC-only CI deploys.** The GitHub Actions deploy workflow
475
+ (`deploy-aws.yml`) accepts **only** GitHub OIDC web-identity
476
+ credentials. `aws-access-key-id` / `aws-secret-access-key` inputs
477
+ are dropped from every `configure-aws-credentials` step in both the
478
+ `deploy` and `deploy-web` jobs; the workflow requires
479
+ `AWS_DEPLOY_ROLE_ARN` and fails fast with `::error::` if it is
480
+ unset, then asserts `aws sts get-caller-identity` matches the
481
+ configured role before any `cdk deploy` (defence-in-depth against a
482
+ silent fallback to ambient static credentials). Long-lived access
483
+ keys powerful enough to deploy CloudFormation + IAM + Lambda +
484
+ DynamoDB are effectively account-admin; a single exfil vector
485
+ yields full account takeover. The full IAM trust policy snippet,
486
+ condition keys, and verification steps are in
487
+ `docs/AWS-Deployment.md` §17. A `ci-hardening` lint + test suite
488
+ (`findAwsCredentialSteps`, `readWorkflowPermissions`) locks the
489
+ OIDC-only contract on every PR.
490
+ - **API Gateway `DataTraceEnabled` off by default and hard-locked.**
491
+ Full-frame body tracing wrote every IRC frame (`PASS <password>`,
492
+ `AUTHENTICATE <base64-SASL-PLAIN>`, `JOIN #chan <key>`,
493
+ `PRIVMSG`/`NOTICE`) to the APIGW execution-log group, leaking
494
+ credentials to anyone with `logs:GetLogEvents`. Now safe-by-default
495
+ (`false`); because the staging/prod split collapsed, there is no
496
+ quiet "non-prod" path to re-enable it. The only way back on is an
497
+ explicit **two-step opt-in** (`-c allowDataTrace=true` **and** `-c
498
+ iUnderstandThisLeaksCredentials=true`) that refuses synthesis unless
499
+ both flags are set — see `docs/AWS-Deployment.md` §7.6. Tear the
500
+ sandbox stack down immediately after debugging; never deploy that
501
+ combination to a shared account.
502
+
503
+ ### Changed — Documentation
504
+
505
+ - **`README.md` updated** to cover AWS web client hosting (the web
506
+ client section was previously Cloudflare-only), the AWS `$connect`
507
+ CSWSH defence, the OIDC-only deploy model, the `DataTraceEnabled`
508
+ hard-lock, and the `scripts/deploy-web-aws.mjs` two-phase deploy.
509
+ The CF-vs-AWS origin difference is laid out in a table, and the
510
+ CSWSH section now documents both adapters' modes (same-origin
511
+ auto-derive on CF; explicit-allowlist-only on AWS).
512
+
513
+ ### ⚠️ Migration required
514
+
515
+ - **Run `tools/migrate-accounts-to-services.ts` BEFORE deploying
516
+ v0.10.0** if the deployment has any rows in the legacy `accounts`
517
+ (CF D1) / `Accounts` (AWS DynamoDB) table. The v0.10.0 build only
518
+ reads `nickserv_accounts` / `Services`, so legacy rows that are not
519
+ backfilled will stop authenticating. The script is one-shot,
520
+ idempotent (`INSERT OR IGNORE` on D1 / conditional `PutItem` on
521
+ DynamoDB), and never-overwrites a nick already registered in the
522
+ services table. It reads the still-deployed `SASL_ACCOUNTS` env var
523
+ at run time so env-seeded accounts are carried through. See the
524
+ script header for the per-platform invocation:
525
+
526
+ ```bash
527
+ # Cloudflare D1
528
+ node --import tsx tools/migrate-accounts-to-services.ts \
529
+ --platform cf --database <d1-name> --remote
530
+
531
+ # AWS DynamoDB
532
+ node --import tsx tools/migrate-accounts-to-services.ts \
533
+ --platform aws --accounts-table Accounts --services-table Services \
534
+ --region us-east-1
535
+ ```
536
+
537
+ - **Single deploy target: rename your deploy commands.** The
538
+ staging-specific commands are gone. Update any scripts, runbooks, or
539
+ CI that called `pnpm deploy:cf:staging` / `pnpm deploy:aws:staging`
540
+ / `--env staging` to the single-target form (`pnpm deploy:cf`,
541
+ `pnpm deploy:aws`). Staging vs production isolation is now by
542
+ account/region (AWS) or by Worker credentials (CF), not by command
543
+ suffix.
544
+ - **AWS CI: switch to OIDC.** If the repo still has
545
+ `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` secrets, delete them
546
+ and invalidate the underlying IAM access key. Configure
547
+ `AWS_DEPLOY_ROLE_ARN` (a GitHub-OIDC-trusted role) per
548
+ `docs/AWS-Deployment.md` §17; there is no static-key fallback.
549
+ - **No persisted-DO-state schema change.** `PERSISTED_STATE_VERSION`
550
+ stays `1`. The `cert_subjects` D1 column is added by an idempotent
551
+ `ALTER TABLE` on cold start; the D1/DynamoDB services-row shape
552
+ change is backward-compatible for reads. **Code rollback across
553
+ this release is safe** provided the account-migration backfill was
554
+ run (rolling back the code re-introduces the legacy `accounts`/
555
+ `Accounts` reads, which still work as long as the table still
556
+ exists — it is not auto-dropped).
557
+
558
+ ### Known limitations
559
+
560
+ - **The web client remains partial.** The Playwright headless-browser
561
+ e2e against a deployed frontend is still pending; the SPA is
562
+ exercised via the WS smoke (`scripts/smoke.mjs`) and the `apps/web`
563
+ unit / build-smoke suite.
564
+ - **The AWS web client `StaticSite` requires a custom domain to
565
+ provision via `bin/aws.ts`.** Provisioning the construct without a
566
+ custom domain (default `*.cloudfront.net` only) currently requires
567
+ editing `bin/aws.ts` to pass `webSite: {}` directly. Tracked as a
568
+ follow-up.
569
+ - Same transport limits as v0.9.0: the **CF TCP path requires
570
+ Cloudflare Spectrum (Enterprise tier)** plus a stateful Container
571
+ origin; the **AWS TCP path** uses NLB + Lambda streaming and is
572
+ subject to Lambda idle-timeout / stream-duration limits. The
573
+ WebSocket path remains the zero-extra-deps default.
574
+ - **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
575
+ CA/trust-store** on both platforms and is not enabled by default.
576
+ The `cert_subjects` plumbing lands the server-side storage; the
577
+ adapter-side client-cert capture at the TLS edge remains a
578
+ follow-up.
579
+ - **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
580
+ TheLounge / matrix-IRC bridge) remains pending.
581
+ - **Coverage hardening is partial.** `irc-core`, `irc-server`,
582
+ `in-memory-runtime`, and **`cf-adapter`** (new this release) sit at
583
+ 100%; `aws-adapter` and `aws-stack` clear the 90% gate. The
584
+ remaining packages (`local-cli`, `load-test`, `cf-tcp-container`,
585
+ `tcp-ws-forwarder`, `irc-test-support`, `web`) are **above the gate
586
+ but below 100%** — follow-ups drive each to full coverage.
587
+
588
+ ### Security
589
+
590
+ - **OIDC-only AWS deploys** (above): removes long-lived access keys
591
+ from the deploy path — the highest-impact exfil vector against an
592
+ AWS account with deploy permissions.
593
+ - **APIGW `DataTraceEnabled` hard-locked off** (above): IRC frames
594
+ (including `PASS`, `AUTHENTICATE <SASL-PLAIN>`, channel keys) are
595
+ no longer written to CloudWatch even if an operator toggles the
596
+ console setting by hand; the two-flag escape hatch cannot be set by
597
+ accident.
598
+ - **CSPRNG-backed `msgid` / session ids** (above): generated ids are
599
+ now cryptographic-grade, closing a forgery vector in the previous
600
+ `Math.random`-based `UuidIdFactory`.
601
+ - No other auth path changed. The scrypt-hashed credential store,
602
+ server-password gate, and CF-side CSWSH defence are unchanged from
603
+ v0.9.0.
604
+
605
+ ---
606
+
13
607
  ## [0.9.0] - 2026-08-10
14
608
 
15
609
  The main change in this release is **rounding out the integrated IRC