serverless-ircd 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/.github/workflows/ci.yml +34 -2
  2. package/.github/workflows/deploy-aws.yml +1 -2
  3. package/.github/workflows/deploy-cf-tcp.yml +1 -5
  4. package/.github/workflows/deploy-cf.yml +1 -2
  5. package/CHANGELOG.md +260 -0
  6. package/README.md +132 -22
  7. package/apps/aws-stack/README.md +33 -0
  8. package/apps/aws-stack/package.json +1 -1
  9. package/apps/aws-stack/src/aws-stack.ts +74 -9
  10. package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
  11. package/apps/aws-stack/tests/stack.test.ts +2 -2
  12. package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
  13. package/apps/aws-stack/tests/vitest.setup.ts +18 -0
  14. package/apps/aws-stack/vitest.config.ts +3 -0
  15. package/apps/cf-tcp-container/package.json +1 -1
  16. package/apps/cf-tcp-container/src/config-loader.ts +6 -7
  17. package/apps/cf-tcp-container/src/container-server.ts +3 -0
  18. package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
  19. package/apps/cf-tcp-container/wrangler.toml +1 -1
  20. package/apps/cf-worker/package.json +2 -1
  21. package/apps/cf-worker/vitest.config.ts +1 -1
  22. package/apps/cf-worker/wrangler.toml +5 -3
  23. package/apps/local-cli/package.json +1 -1
  24. package/apps/local-cli/src/config-loader.ts +6 -6
  25. package/apps/local-cli/src/main.ts +20 -0
  26. package/apps/local-cli/src/server.ts +9 -7
  27. package/apps/local-cli/tests/e2e.test.ts +112 -28
  28. package/apps/local-cli/tests/tcp.test.ts +71 -19
  29. package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
  30. package/biome.json +1 -0
  31. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  32. package/docs/ADR-002-location-of-authority.md +82 -0
  33. package/docs/ADR-003-durable-object-sharding.md +110 -0
  34. package/docs/ADR-004-dynamodb-schema.md +96 -0
  35. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  36. package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
  37. package/docs/ADR-007-deterministic-ports.md +82 -0
  38. package/docs/ADR-008-monorepo-tooling.md +60 -0
  39. package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
  40. package/docs/ADR-Index.md +15 -0
  41. package/docs/AWS-Adapter-Architecture.md +496 -0
  42. package/docs/AWS-Deployment.md +1275 -0
  43. package/docs/AWS-TCP-Deployment.md +410 -0
  44. package/docs/Cloudflare-Deployment-Guide.md +719 -0
  45. package/docs/Cloudflare-TCP-Deployment.md +510 -0
  46. package/docs/Home.md +15 -0
  47. package/docs/Observability.md +87 -0
  48. package/docs/PlanExtensions.md +812 -0
  49. package/docs/PlanIRCv3Websocket.md +503 -0
  50. package/docs/PlanWebClient.md +451 -0
  51. package/docs/Release-Process.md +443 -0
  52. package/package.json +2 -2
  53. package/packages/aws-adapter/package.json +1 -1
  54. package/packages/aws-adapter/src/handlers/default.ts +3 -0
  55. package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
  56. package/packages/aws-adapter/src/serialize.ts +29 -1
  57. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
  58. package/packages/aws-adapter/tests/account-store.test.ts +4 -7
  59. package/packages/aws-adapter/tests/aws-harness.ts +5 -2
  60. package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
  61. package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
  62. package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
  63. package/packages/aws-adapter/tests/connect.test.ts +9 -9
  64. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
  65. package/packages/aws-adapter/tests/global-setup.ts +55 -30
  66. package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
  67. package/packages/aws-adapter/tests/handlers.test.ts +90 -4
  68. package/packages/aws-adapter/tests/message-store.test.ts +6 -5
  69. package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
  70. package/packages/cf-adapter/package.json +2 -1
  71. package/packages/cf-adapter/src/connection-do.ts +3 -0
  72. package/packages/cf-adapter/src/serialize.ts +25 -4
  73. package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
  74. package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
  75. package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
  76. package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
  77. package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
  78. package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
  79. package/packages/cf-adapter/tests/serialize.test.ts +308 -0
  80. package/packages/cf-adapter/vitest.config.ts +1 -1
  81. package/packages/in-memory-runtime/package.json +1 -1
  82. package/packages/irc-core/package.json +1 -1
  83. package/packages/irc-core/src/caps/capabilities.ts +37 -3
  84. package/packages/irc-core/src/caps/index.ts +1 -0
  85. package/packages/irc-core/src/caps/sts.ts +84 -0
  86. package/packages/irc-core/src/commands/account-notify.ts +53 -0
  87. package/packages/irc-core/src/commands/away.ts +9 -3
  88. package/packages/irc-core/src/commands/cap.ts +23 -5
  89. package/packages/irc-core/src/commands/chathistory.ts +40 -11
  90. package/packages/irc-core/src/commands/index.ts +34 -0
  91. package/packages/irc-core/src/commands/isupport.ts +53 -0
  92. package/packages/irc-core/src/commands/mode.ts +8 -1
  93. package/packages/irc-core/src/commands/monitor.ts +327 -0
  94. package/packages/irc-core/src/commands/multiline.ts +256 -0
  95. package/packages/irc-core/src/commands/pre-away.ts +112 -0
  96. package/packages/irc-core/src/commands/privmsg.ts +19 -12
  97. package/packages/irc-core/src/commands/read-marker.ts +108 -0
  98. package/packages/irc-core/src/commands/registration.ts +8 -0
  99. package/packages/irc-core/src/commands/sasl.ts +24 -1
  100. package/packages/irc-core/src/commands/tagmsg.ts +71 -1
  101. package/packages/irc-core/src/commands/whois.ts +5 -0
  102. package/packages/irc-core/src/config.ts +65 -10
  103. package/packages/irc-core/src/effects.ts +14 -0
  104. package/packages/irc-core/src/ports.ts +389 -0
  105. package/packages/irc-core/src/protocol/index.ts +14 -0
  106. package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
  107. package/packages/irc-core/src/protocol/numerics.ts +15 -0
  108. package/packages/irc-core/src/protocol/outbound.ts +16 -1
  109. package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
  110. package/packages/irc-core/src/state/connection.ts +32 -1
  111. package/packages/irc-core/src/types.ts +112 -0
  112. package/packages/irc-core/stryker.commands.conf.json +1 -2
  113. package/packages/irc-core/tests/away-store.test.ts +73 -0
  114. package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
  115. package/packages/irc-core/tests/caps/sts.test.ts +123 -0
  116. package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
  117. package/packages/irc-core/tests/commands/away.test.ts +85 -2
  118. package/packages/irc-core/tests/commands/cap.test.ts +165 -0
  119. package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
  120. package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
  121. package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
  122. package/packages/irc-core/tests/commands/mode.test.ts +72 -0
  123. package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
  124. package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
  125. package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
  126. package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
  127. package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
  128. package/packages/irc-core/tests/commands/registration.test.ts +64 -0
  129. package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
  130. package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
  131. package/packages/irc-core/tests/commands/whois.test.ts +34 -0
  132. package/packages/irc-core/tests/config.test.ts +103 -13
  133. package/packages/irc-core/tests/labeled-response.test.ts +181 -0
  134. package/packages/irc-core/tests/message-tags.test.ts +114 -0
  135. package/packages/irc-core/tests/monitor-store.test.ts +200 -0
  136. package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
  137. package/packages/irc-core/tests/standard-replies.test.ts +412 -0
  138. package/packages/irc-core/tests/types.test.ts +27 -0
  139. package/packages/irc-server/package.json +1 -1
  140. package/packages/irc-server/src/actor.ts +450 -3
  141. package/packages/irc-server/src/dispatch.ts +108 -16
  142. package/packages/irc-server/tests/actor.test.ts +1432 -39
  143. package/packages/irc-server/tests/dispatch.test.ts +263 -2
  144. package/packages/irc-test-support/package.json +1 -1
  145. package/packages/irc-test-support/src/index.ts +6 -0
  146. package/packages/irc-test-support/src/test-config.ts +54 -0
  147. package/packages/irc-test-support/tests/test-config.test.ts +51 -0
  148. package/tools/ci-hardening/package.json +1 -1
  149. package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
  150. package/tools/load-test/package.json +33 -0
  151. package/tools/load-test/src/client.ts +351 -0
  152. package/tools/load-test/src/config.ts +313 -0
  153. package/tools/load-test/src/harness.ts +116 -0
  154. package/tools/load-test/src/main.ts +120 -0
  155. package/tools/load-test/src/metrics.ts +168 -0
  156. package/tools/load-test/src/report.ts +106 -0
  157. package/tools/load-test/tests/client.test.ts +212 -0
  158. package/tools/load-test/tests/config.test.ts +152 -0
  159. package/tools/load-test/tests/framing.test.ts +37 -0
  160. package/tools/load-test/tests/harness.test.ts +165 -0
  161. package/tools/load-test/tests/metrics.test.ts +174 -0
  162. package/tools/load-test/tests/report.test.ts +161 -0
  163. package/tools/load-test/tests/smoke.test.ts +67 -0
  164. package/tools/load-test/tsconfig.build.json +12 -0
  165. package/tools/load-test/tsconfig.test.json +10 -0
  166. package/tools/load-test/vitest.config.ts +29 -0
  167. package/tools/tcp-ws-forwarder/package.json +1 -1
@@ -42,6 +42,32 @@ jobs:
42
42
  node-version: 24
43
43
  cache: pnpm
44
44
 
45
+ # DynamoDB Local capability for `aws-adapter` coverage.
46
+ #
47
+ # The `aws-adapter` package's vitest `globalSetup` boots a real
48
+ # DynamoDB Local instance so the integration suite (handlers,
49
+ # aws-runtime, transactions, …) can be exercised under coverage.
50
+ # Without it, 171 of 294 tests `describe.skipIf` away and the
51
+ # package drops to ~42% coverage, blowing the 90% gate.
52
+ #
53
+ # The self-hosted Gitea runner image (`ubuntu-latest`) ships with
54
+ # neither Docker (testcontainers' Ryuk reaper fails with "Failed
55
+ # to connect to Reaper" without a `docker.sock` mount or
56
+ # privileged mode) nor a JRE. We install a JRE here via the
57
+ # canonical `setup-java` action so the globalSetup's ZIP fallback
58
+ # can fire — this is the only strategy the runner image can
59
+ # support without host-level privileged-mode configuration.
60
+ #
61
+ # `DDB_LOCAL_STRATEGY=zip` is pinned on the coverage step below
62
+ # so the globalSetup skips the testcontainers probe entirely
63
+ # (otherwise it blocks for ~60s waiting for Ryuk before falling
64
+ # back, which is flaky and wastes CI minutes).
65
+ - name: Setup Java (JRE for DynamoDB Local ZIP fallback)
66
+ uses: actions/setup-java@v4
67
+ with:
68
+ distribution: temurin
69
+ java-version: '11'
70
+
45
71
  - name: Install dependencies
46
72
  run: pnpm install --frozen-lockfile
47
73
 
@@ -52,6 +78,12 @@ jobs:
52
78
  run: pnpm typecheck
53
79
 
54
80
  - name: Test with coverage
81
+ # `DDB_LOCAL_STRATEGY=zip` pins the DynamoDB Local boot to the
82
+ # Java ZIP fallback (see the `Setup Java` step above for why).
83
+ # This skips the testcontainers probe so the runner does not
84
+ # block ~60s waiting for Ryuk before falling back.
85
+ env:
86
+ DDB_LOCAL_STRATEGY: zip
55
87
  run: pnpm coverage
56
88
 
57
89
  - name: Coverage gate (coverage-thresholds enforced per package)
@@ -90,7 +122,7 @@ jobs:
90
122
 
91
123
  - name: Upload coverage
92
124
  if: always()
93
- uses: actions/upload-artifact@v4
125
+ uses: actions/upload-artifact@v3
94
126
  with:
95
127
  name: coverage
96
128
  path: |
@@ -135,7 +167,7 @@ jobs:
135
167
 
136
168
  - name: Upload mutation report
137
169
  if: always()
138
- uses: actions/upload-artifact@v4
170
+ uses: actions/upload-artifact@v3
139
171
  with:
140
172
  name: mutation-report
141
173
  path: packages/irc-core/reports/mutation
@@ -38,8 +38,7 @@ name: deploy-aws
38
38
  # behaviour).
39
39
 
40
40
  on:
41
- push:
42
- branches: [main]
41
+ # Automatic push trigger disabled; deploy only runs manually.
43
42
  workflow_dispatch:
44
43
 
45
44
  # OIDC web identity requires id-token write; contents:read for checkout.
@@ -16,11 +16,7 @@ name: deploy-cf-tcp
16
16
  # CF_TCP_ORIGIN_ADDRESS Hostname/IP of the container origin.
17
17
 
18
18
  on:
19
- push:
20
- branches: [main]
21
- paths:
22
- - 'apps/cf-tcp-container/**'
23
- - '.github/workflows/deploy-cf-tcp.yml'
19
+ # Automatic push trigger disabled; deploy only runs manually.
24
20
  workflow_dispatch:
25
21
 
26
22
  concurrency:
@@ -25,8 +25,7 @@ name: deploy-cf
25
25
  # custom routes are wired.
26
26
 
27
27
  on:
28
- push:
29
- branches: [main]
28
+ # Automatic push trigger disabled; deploy only runs manually.
30
29
  workflow_dispatch:
31
30
 
32
31
  # Prevent two deploys racing the same DO namespace.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,266 @@ 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.6.0] - 2026-08-03
14
+
15
+ The headline is the **IRCv3 extension sweep (Phase 12)**: ten negotiated
16
+ caps and the user-mode-`S` TLS indicator land in one release, taking the
17
+ negotiated capability surface from "modern baseline" to "draft-leading-
18
+ edge". Alongside it: the **required-`serverName` config gate** (the
19
+ last open v0.5.0 follow-up), a new **`tools/load-test`** client farm
20
+ (Phase 7's 10k-connection harness), and the **coverage / CI unblocks**
21
+ that lift `aws-adapter` over the 90% gate and switch `cf-adapter` /
22
+ `cf-worker` to istanbul (clearing the v0.5.0 workerd-incompatibility
23
+ caveat). Deploy workflows are now **manual-only** (`workflow_dispatch`).
24
+
25
+ ### Added — IRCv3 extensions (`@serverless-ircd/irc-core`, `@serverless-ircd/irc-server`)
26
+
27
+ The ten Phase 12 cap tickets light up the remaining negotiated-cap
28
+ surface, and the two non-cap Phase 12 tickets (ISUPPORT tokens and
29
+ user mode `S`) round out the wire-visible advertisement. Each cap
30
+ ships a pure reducer (where applicable), an actor route case,
31
+ advertisement in `SUPPORTED_CAPABILITIES` (or conditional
32
+ advertisement for `sts`), and unit + actor coverage. The
33
+ pure-reducer / dispatch pipeline is unchanged — every cap is plumbed
34
+ through `ctx` and emitted `Effect[]`, never performed in the handler.
35
+
36
+ - **`account-notify`**: pushes `:nick ACCOUNT <name|*>`
37
+ to peers in shared channels (excluding the sender unless
38
+ `echo-message`) on SASL login / logout. Closes the
39
+ `account-tag` / `extended-join` / `account-notify` triple modern
40
+ clients render fully.
41
+ - **`msgid`**: every PRIVMSG / NOTICE / TAGMSG fanned out
42
+ to a cap-enabled peer carries `@+msgid=<id>` from
43
+ `IdFactory.nonce()`. The nonce is **shared between the live fanout
44
+ and the `draft/chathistory` record** so a peer sees the same id live
45
+ and on replay. Legacy peers see no `msgid` tag.
46
+ - **`standard-replies`**: a data-driven
47
+ `numericToStandardReply` table replaces a curated subset of bare
48
+ numerics (`461`, `403`, `432`, `433`, SASL failures, …) with
49
+ `FAIL`/`WARN`/`NOTE COMMAND code context :description` for
50
+ cap-enabled clients. Legacy clients still receive the unchanged
51
+ numeric. Numeric-only replies (MOTD lines, NAMES, …) stay on their
52
+ numeric form.
53
+ - **`MONITOR`**: the modern `ISON` replacement. Implements
54
+ `MONITOR + nick{,nick}` / `- nick` / `C` / `L` / `S` with the
55
+ `730 RPL_MONONLINE` / `731 RPL_MONOFFLINE` / `732 RPL_MONLIST` /
56
+ `734 ERR_MONLISTFULL` numerics. Online/offline transitions push
57
+ `730`/`731` **asynchronously** via the Registry's nick reserve /
58
+ release hooks (a new `MonitorStore` port + push callback). The
59
+ ceiling is config-driven (`MONITOR=<n>`, default 30).
60
+ - **`labeled-response`**: a client-sent `+label=<id>` is
61
+ plumbed through `ctx` and every reply caused by that message is
62
+ wrapped in a single `BATCH +id labeled-response … BATCH -id`. A
63
+ command that produces no reply emits an empty labeled batch (spec-
64
+ mandated). The label travels on every reducer's emitted effects,
65
+ so multi-reply commands accumulate under one batch.
66
+ - **`sts`**: Strict Transport Security. The cap is
67
+ **conditionally advertised** — only when `ServerConfig.sts`
68
+ (`{ duration, port, preload? }`) is supplied. On a plaintext
69
+ listener a compliant client upgrades; on the TLS listener the
70
+ duration is still sent to lock in the policy. Defaults:
71
+ `duration = 30 days`, `port = 6697`.
72
+ - **`draft/typing`**: `TAGMSG <target> +draft/typing=
73
+ active|paused|done` broadcasts to channel members (or routes to a
74
+ nick target), excluding the sender unless `echo-message`. The
75
+ `+draft/typing=*` client tag is whitelisted for clients without
76
+ `message-tags` (per the spec's client-tag exception). Broadcast
77
+ delivery uses **OR-semantics**: a recipient qualifies if it
78
+ negotiated `message-tags` **or** `draft/typing` (a new `caps` field
79
+ on `BroadcastEffect`).
80
+ - **`draft/multiline`**: a cap-enabled
81
+ client sends `BATCH +id draft/multiline :target` + N PRIVMSG/NOTICE
82
+ lines + `BATCH -id`. The server concatenates per spec (with `\n`),
83
+ enforces a configurable byte budget (`multilineMaxBytes`, default
84
+ 4096), and broadcasts the batched replay to multiline peers / a
85
+ single joined PRIVMSG (carrying `+draft/multiline`) to legacy
86
+ peers. **Nick-target routing**, **chathistory recording** (one
87
+ record per batch carrying the joined body, assigned `msgid`, and
88
+ the `+draft/multiline` client tag), and **tag gating** (the
89
+ `+draft/multiline` tag is stripped for peers without
90
+ `message-tags`) all land in the completion ticket.
91
+ - **`draft/read-marker`**: persists the per-`(account,
92
+ channel)` last-read marker in a new `ReadMarkerStore` port,
93
+ propagates updates via `+draft/read-marker=<msgid>` tags, and
94
+ seeds a fresh connection's marker from the persisted value at
95
+ identify time. `CHATHISTORY BEFORE` honours the marker when no
96
+ explicit pivot is supplied.
97
+ - **`draft/pre-away`**: persists the away reason in a
98
+ new `AwayStore` port keyed by account. On identify (or SASL login)
99
+ the server re-applies the away state and emits the `AWAY` line +
100
+ `306 RPL_NOWAWAY` as if the user had sent `AWAY :reason`
101
+ themselves. `AWAY` (unset) clears the persisted reason.
102
+ - **ISUPPORT tokens for the new caps**: `005
103
+ RPL_ISUPPORT` now advertises `STATUSMSG=@+`, `EXTBAN=<prefix>,q`,
104
+ `ACCOUNTEXTBAN=a` (services-gated), `TYPING`, and `MULTILINE`.
105
+ `MONITOR=<n>` honours `serverConfig.monitorLimit`. Tokens are
106
+ config-derived where applicable.
107
+ - **User mode `S` (TLS connected)**: the transport's
108
+ TLS fact is surfaced end-to-end as a read-only user mode. Set
109
+ once by the registration reducer from `ConnectionState.secure`;
110
+ emitted in `221 … +S`; rejected by `MODE nick +/-S` with `501
111
+ ERR_UMODEUNKNOWNFLAG`; surfaced in WHOIS via the new `276
112
+ RPL_WHOISSECURE`. Each adapter asserts `secure: true` where the
113
+ platform terminates TLS (API Gateway, CF edge, Spectrum, NLB TLS
114
+ listener).
115
+
116
+ ### Changed — Required server identity
117
+
118
+ The last open v0.5.0 follow-up closes. The `irc.example.com`
119
+ placeholder `DEFAULT_SERVER_CONFIG` is **removed** from
120
+ `irc-core`; the shared `ServerConfigSchema` rejects a missing
121
+ `serverName` / `networkName` so every adapter **fails fast at boot**
122
+ with a readable, field-listing error.
123
+
124
+ - **`@serverless-ircd/irc-core`**: `DEFAULT_SERVER_CONFIG` removed.
125
+ - **`@serverless-ircd/irc-test-support`**: ships a clearly test-scoped
126
+ `makeTestServerConfig()` baseline (`irc.test.local`) so unit tests
127
+ construct a `ServerConfig` without re-introducing a production
128
+ default.
129
+ - `apps/cf-worker` + `apps/cf-tcp-container`: loaders require
130
+ `SERVER_NAME`; `wrangler.toml` samples switched to
131
+ `irc.your-domain.invalid`.
132
+ - `apps/aws-stack` + `apps/local-cli`: keep obviously-local fallbacks
133
+ (`irc.localhost`) documented as *"production MUST override"*;
134
+ `local-cli` gains `--server-name` / `--network-name` flags.
135
+
136
+ ### Added — Load-test harness (`@serverless-ircd/load-test`)
137
+
138
+ PLAN §7 / Phase 7's 10k-connection harness. A synthetic WebSocket IRC
139
+ client farm: opens N connections, registers (NICK/USER), joins a
140
+ channel, and optionally chats. Captures **p50 / p95 / p99 latency per
141
+ phase** (connect / register / join / message) plus the **drop rate**,
142
+ and prints a markdown report to stdout (or `--report <path>`).
143
+
144
+ ```bash
145
+ pnpm --filter load-test start -- \
146
+ --target wss://irc.staging.example.com/ \
147
+ --connections 10000 --concurrency 200 --ramp-ms 60000 \
148
+ --channel '#loadtest' --messages 3 --platform cf-staging \
149
+ --report reports/cf-staging-2026-08-03.md
150
+ ```
151
+
152
+ The package ships its own vitest suite (config parsing, framing,
153
+ harness, metrics, report) at ≥90% coverage; no production source in
154
+ the irc-core / irc-server packages changed to support it.
155
+
156
+ ### Added — Coverage / CI unblocks
157
+
158
+ - **`aws-adapter` clears the 90% gate.** The self-hosted runner image
159
+ ships with neither Docker nor a JRE, so the package's
160
+ testcontainers / Java-ZIP DynamoDB Local boot fell through to
161
+ strategy 3 and 171 of 294 integration tests `describe.skipIf`-ed
162
+ away — dropping coverage to ~42% and blowing the gate. CI now
163
+ installs Temurin JRE 11 via `actions/setup-java@v4` and pins
164
+ `DDB_LOCAL_STRATEGY=zip` on the coverage step so the ZIP fallback
165
+ fires deterministically. The full aws-adapter suite now runs on
166
+ every coverage job. New integration tests cover
167
+ `broadcastWallops` fan-out, `reserveNick` non-conditional error
168
+ propagation, `refreshChannel` empty-members / topic-preservation
169
+ paths, NLB header fallbacks, scan-row type guards, and
170
+ `CREATED_AT` overflow handling. `tools/ci-hardening` gained a
171
+ self-check asserting the JRE install + strategy pin remain wired.
172
+ - **`aws-stack` coverage command repaired.** The CDK `NodejsFunction`
173
+ in the test synth re-bundled the Lambda asset via esbuild ~80
174
+ times per coverage run and OOM-ed CI before vitest could print a
175
+ report. A new `makeLambda()` factory switches between
176
+ `NodejsFunction` (production synth) and an inline-zip `Function`
177
+ stub (tests) via the `IRC_AWS_STACK_TEST_NO_BUNDLE` env var. The
178
+ stub preserves every CloudFormation shape the suite asserts on
179
+ while making bundling a no-op. New
180
+ `tests/synth-no-bundle.test.ts` + `tests/coverage-command.test.ts`
181
+ lock both branches and assert the `pnpm coverage` run prints an
182
+ `All files` row.
183
+ - **`cf-adapter` / `cf-worker` switch to istanbul coverage.** This
184
+ **resolves the v0.5.0 known limitation**: `@vitest/coverage-v8`
185
+ v4.1.x imports `node:inspector/promises`, which the
186
+ `@cloudflare/vitest-pool-workers` (workerd) pool cannot resolve.
187
+ Both packages now use `@vitest/coverage-istanbul` so `pnpm
188
+ coverage` loads under workerd. Edge-case suites were expanded
189
+ (serialize/deserialize/migrate, connection-do pure helpers,
190
+ `broadcastWallops` fan-out, `changeNick` shard routing,
191
+ ChannelDO broadcast/cache, RegistryDO `listEntries`, SASL seed
192
+ parsing).
193
+
194
+ ### Changed — Deploy workflows are now manual-only
195
+
196
+ `deploy-cf.yml`, `deploy-cf-tcp.yml`, and `deploy-aws.yml` dropped
197
+ their `push: branches: [main]` triggers and now run on
198
+ `workflow_dispatch` only. Staging deploys are no longer fired
199
+ automatically on every merge to `main`; a maintainer kicks them off
200
+ by hand. The release runbook (`docs/Release-Process.md`) is the
201
+ canonical reference either way — there was never a CI auto-deploy to
202
+ production.
203
+
204
+ ### Changed — `@vitest/coverage-istanbul` peer
205
+
206
+ The root lockfile (`pnpm-lock.yaml`) registers the
207
+ `@vitest/coverage-istanbul` peer introduced by the cf-adapter /
208
+ cf-worker coverage switch. No operator action; `pnpm install`
209
+ resolves it.
210
+
211
+ ### ⚠️ Migration required
212
+
213
+ - **`serverName` is now required at boot.** Operators who relied on
214
+ the silent `irc.example.com` placeholder must supply an explicit
215
+ value via their adapter's config knob:
216
+ - **Cloudflare Worker / TCP container**: set `SERVER_NAME` (and
217
+ `NETWORK_NAME` if desired) in `wrangler.toml` `[vars]` or via
218
+ `wrangler secret put`. The samples switched to
219
+ `irc.your-domain.invalid` — replace with your real hostname.
220
+ - **AWS**: the CDK stack's `serverName` prop / `SERVER_NAME` env
221
+ var. The local fallback is `irc.localhost` and is explicitly
222
+ documented as "production MUST override".
223
+ - **`local-cli`**: new `--server-name <h>` / `--network-name <n>`
224
+ flags (default `irc.localhost` / `LocalNet`).
225
+ A missing value now throws an aggregated `"Invalid server
226
+ config:"` error at boot listing every offending field.
227
+ - **No persisted-state schema change.** `PERSISTED_STATE_VERSION`
228
+ stays `1`. The user-mode-`S` work adds a `secure` field and a
229
+ `userModes.tls` field to the persisted shape, but both are purely
230
+ **additive with `false` defaults** and the cf-adapter `migrate()`
231
+ path backfills them on pre-feature records. **Code rollback across
232
+ this release is safe.**
233
+ - No toolchain change. Node ≥ 24 / pnpm 11 (from v0.4.0) still apply.
234
+
235
+ ### Known limitations
236
+
237
+ - Same transport limits as v0.5.0: the **CF TCP path requires
238
+ Cloudflare Spectrum (Enterprise tier)** plus a stateful Container
239
+ origin; the **AWS TCP path** uses NLB + Lambda streaming and is
240
+ subject to Lambda idle-timeout / stream-duration limits. The
241
+ WebSocket path remains the zero-extra-deps default.
242
+ - **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
243
+ CA/trust-store** on both platforms and is not enabled by default.
244
+ - `STATS` ships the `u` + `l` query letters; the full RFC matrix
245
+ (`k`, `K`, `q`, `g`, `x`, `z`, …) is intentionally out of scope for
246
+ v1 — unknown letters get the charybdis-style empty body + `219`.
247
+ - **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
248
+ TheLounge / matrix-IRC bridge) remains pending.
249
+ - **Coverage hardening is partial.** `aws-adapter` and `aws-stack`
250
+ now clear the 90% gate, and `cf-adapter` / `cf-worker` run under
251
+ istanbul; the remaining packages (`cf-adapter`, `local-cli`,
252
+ `load-test`, `cf-tcp-container`, `tcp-ws-forwarder`,
253
+ `irc-test-support`) are **above the gate but below 100%** —
254
+ follow-ups drive each to 100%.
255
+ - The IRC services (NickServ / ChanServ / HostServ / OperServ /
256
+ MemoServ) Phase 13 work is pending; `draft/read-marker` and
257
+ `draft/pre-away` ship with in-memory reference stores, so marker /
258
+ away persistence does not survive an adapter restart until a
259
+ services backend lands.
260
+ - The web client Phase 14 work (vendor Kiwi IRC, serve SPA from the
261
+ Worker, WS Origin allowlist) is pending.
262
+
263
+ ### Security
264
+
265
+ - None. No vulnerabilities have been reported or fixed in this
266
+ release. The new caps add surface area but ship no new auth path
267
+ (the SASL `PLAIN` / `EXTERNAL` paths and the oper gate are
268
+ unchanged); the required-`serverName` change rejects misconfigured
269
+ deploys at boot rather than advertising a placeholder identity.
270
+
271
+ ---
272
+
13
273
  ## [0.5.0] - 2026-08-02
14
274
 
15
275
  The headline is **protocol completion**: the deferred IRC verbs land
package/README.md CHANGED
@@ -6,7 +6,7 @@ platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
6
6
 
7
7
  One TypeScript codebase. Two serverless substrates.
8
8
 
9
- > **Status:** **v0.5.0 (preview).** The pure protocol core, the
9
+ > **Status:** **v0.6.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
@@ -15,15 +15,25 @@ One TypeScript codebase. Two serverless substrates.
15
15
  > clients — Cloudflare via Spectrum + a Container origin, AWS via a
16
16
  > Network Load Balancer + Lambda streaming — alongside the WebSocket
17
17
  > path, plus **SASL `EXTERNAL` via mTLS** and a **Cloudflare D1-backed
18
- > SASL account store**. v0.5.0 completes the **protocol surface**: the
18
+ > SASL account store**. v0.5.0 completed the **protocol surface**: the
19
19
  > deferred IRC verbs (`KILL`, `REHASH`, `LUSERS` + `STATS`, `TRACE`,
20
20
  > `WALLOPS`, `SETNAME`) all land, and the S2S (`CONNECT` / `SQUIT` /
21
21
  > `LINKS`) and obsolete RFC 2812 (`SERVICE` / `SUMMON` / `USERS`) verbs
22
- > are formally dropped every standard verb a modern client expects is
23
- > now implemented. Remaining 0.x work: the load/compat sweep
24
- > (10k-connection load test + client compatibility matrix) and a
25
- > required-`serverName` config gate. See `CHANGELOG.md` for the
26
- > per-release manifests.
22
+ > are formally dropped. **v0.6.0 lands the IRCv3 extension sweep
23
+ > (Phase 12)** ten caps (`account-notify`, `msgid`,
24
+ > `standard-replies`, `MONITOR`, `labeled-response`, `sts`,
25
+ > `draft/typing`, `draft/multiline`, `draft/read-marker`,
26
+ > `draft/pre-away`), the ISUPPORT tokens for the new caps, and the
27
+ > read-only user mode `S` (TLS connected) — closes the
28
+ > required-`serverName` config gate, ships a `tools/load-test`
29
+ > 10k-connection harness, and lifts `aws-adapter` / `aws-stack` over
30
+ > the 90% coverage gate (CF packages moved to istanbul coverage,
31
+ > clearing the v0.5.0 workerd-incompatibility caveat). Remaining 0.x
32
+ > work: the client compatibility sweep (WeeChat / HexChat / IRCCloud /
33
+ > TheLounge), IRC services (NickServ / ChanServ / HostServ /
34
+ > OperServ / MemoServ), a web client (vendored Kiwi IRC), and driving
35
+ > the remaining sub-100% packages to full coverage. See
36
+ > `CHANGELOG.md` for the per-release manifests.
27
37
 
28
38
  ---
29
39
 
@@ -130,6 +140,7 @@ ServerlessIRCd/
130
140
  │ └── local-cli/ runnable WS + TCP server using in-memory-runtime
131
141
  ├── tools/
132
142
  │ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
143
+ │ ├── load-test/ synthetic WebSocket IRC client farm (10k conns, p50/p95/p99, drop rate)
133
144
  │ ├── ci-hardening/ coverage-gate + mutation-config validators
134
145
  │ ├── seed-aws-accounts.ts scrypt-hash SASL PLAIN accounts into DynamoDB
135
146
  │ └── seed-cf-accounts.ts scrypt-hash SASL PLAIN accounts into Cloudflare D1
@@ -230,6 +241,8 @@ On startup it logs a JSON line like:
230
241
  | `--tcp-port <n>` | `<port> + 1` | TCP port for RFC-style IRC clients. |
231
242
  | `--no-tcp` | | Disable the TCP listener (WebSocket only). |
232
243
  | `--motd-file <p>` | built-in | Read MOTD lines from this file (one per line). |
244
+ | `--server-name <h>`| `irc.localhost` | Server hostname advertised in `001`/`005`. Production MUST override. |
245
+ | `--network-name <n>`| `LocalNet` | Network name advertised in `005 NETWORK=…`. |
233
246
  | `-h, --help` | | Show help and exit. |
234
247
 
235
248
  Pass `--host 0.0.0.0` to expose the server on all interfaces. `SIGINT` /
@@ -302,6 +315,55 @@ against any line-oriented WebSocket endpoint.
302
315
 
303
316
  ---
304
317
 
318
+ ## Load testing (`tools/load-test`)
319
+
320
+ A synthetic WebSocket IRC client farm (PLAN §7, Phase 7). It opens N
321
+ connections to a `ws://` / `wss://` target, registers each (NICK/USER),
322
+ joins a channel, and optionally chats. Per-phase **p50 / p95 / p99
323
+ latency** (connect / register / join / message) and the **drop rate**
324
+ are captured, then printed as a markdown report to stdout (and written
325
+ to `--report <path>` when given).
326
+
327
+ Build the workspace, then start a run (from the repo root):
328
+
329
+ ```bash
330
+ pnpm build
331
+
332
+ pnpm --filter load-test start -- \
333
+ --target wss://irc.staging.example.com/ \
334
+ --connections 10000 --concurrency 200 --ramp-ms 60000 \
335
+ --channel '#loadtest' --messages 3 --platform cf-staging \
336
+ --report reports/cf-staging-2026-08-03.md
337
+ ```
338
+
339
+ The harness drives the WebSocket transport only (the serverless default);
340
+ a TCP IRC client is not what a serverless load test exercises. Each
341
+ connection is independent — the package ships no `@serverless-ircd/*`
342
+ runtime dependency (only the IRC line framing shared with the
343
+ forwarder), so it can be pointed at any WebSocket IRC endpoint.
344
+
345
+ ### CLI flags
346
+
347
+ | Flag | Default | Description |
348
+ |-------------------------|-------------|----------------------------------------------------------|
349
+ | `--target <url>` | (required) | Upstream `ws://` or `wss://` URL to load test. |
350
+ | `--connections <n>` | `1000` | Total connections to open. |
351
+ | `--concurrency <n>` | `50` | Concurrent in-flight connection attempts during ramp-up. |
352
+ | `--ramp-ms <ms>` | `30000` | Ramp-up duration over which connections are opened. |
353
+ | `--channel <c>` | `#loadtest` | Channel every client JOINs. |
354
+ | `--messages <n>` | `0` | PRIVMSGs each client sends after JOIN (`0` = connect only). |
355
+ | `--echo-message` | | Negotiate `echo-message` so sent PRIVMSGs round-trip. |
356
+ | `--platform <name>` | `unknown` | Label baked into the report header. |
357
+ | `--report <path>` | | Write the markdown report to this path. |
358
+ | `-h, --help` | | Show help and exit. |
359
+
360
+ `SIGINT` / `SIGTERM` cancel an in-flight run gracefully (drains live
361
+ sockets, prints the partial report). The package ships its own vitest
362
+ suite at ≥90% coverage (config parsing, framing, harness, metrics,
363
+ report).
364
+
365
+ ---
366
+
305
367
  ## WebSocket transport (IRCv3 subprotocols)
306
368
 
307
369
  The WebSocket entry points (the local CLI, the Cloudflare Worker, and the
@@ -373,21 +435,23 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
373
435
  - **Phase 2** — `IrcRuntime` port, in-memory runtime, `ConnectionActor`,
374
436
  local CLI, parametrized contract suite. ✅ landed in **v0.1.0**.
375
437
  - **Phase 3** — Cloudflare adapter (ConnectionDO / ChannelDO / RegistryDO).
376
- ✅ landed in **v0.1.0** (staging auto-deploy via CI; prod deploy is manual).
438
+ ✅ landed in **v0.1.0** (staging deploy via CI; prod deploy is manual).
377
439
  - **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
378
- ✅ landed in **v0.2.0** (staging auto-deploy via CI; prod deploy is manual).
440
+ ✅ landed in **v0.2.0** (staging deploy via CI; prod deploy is manual).
379
441
  - **Phase 5** — Observability, security hardening, config, CI gates.
380
442
  ✅ landed across v0.2.0–v0.4.0 (logger port, server-password, cloaking,
381
443
  admission limits, coverage + mutation gates, OPER credential auth, the
382
- Cloudflare config loader, and AWS max-clients admission).
444
+ Cloudflare config loader, and AWS max-clients admission); the
445
+ required-`serverName` gate closed in **v0.6.0**.
383
446
  - **Phase 6** — TLS hardening & raw TCP transport (`irc+tls :6697`).
384
447
  ✅ landed in **v0.4.0**: generalized `ConnectionActor` transport seam,
385
448
  Cloudflare Spectrum + Container origin, AWS NLB + Lambda streaming,
386
449
  mTLS → SASL `EXTERNAL`, transport-parametrized contract suite, ADR-009.
387
- - **Phase 7** — Load testing (10k concurrent connections per platform)
388
- and client compatibility sweep (WeeChat, HexChat, IRCCloud, TheLounge).
389
- pending; the dual-transport surface it needs
390
- landed in v0.4.0.
450
+ - **Phase 7 — Load testing & compatibility sweep.** 🔄 partial as of
451
+ **v0.6.0**: the `tools/load-test` 10k-connection harness is in-tree;
452
+ the formal 10k-connection report per platform and the client
453
+ compatibility matrix (WeeChat / HexChat / IRCCloud / TheLounge) are
454
+ still pending.
391
455
  - **Phase 8 — PLAN-FIXES remediation.** ✅ landed across v0.3.0–v0.4.0
392
456
  (MessageStore wired into all three adapters, AccountStore end-to-end,
393
457
  the DynamoDB `Accounts` table decided, CF enumeration RPCs, AWS QUIT
@@ -406,6 +470,28 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
406
470
  the formal drop of the S2S (`CONNECT`/`SQUIT`/`LINKS`) and obsolete
407
471
  RFC 2812 (`SERVICE`/`SUMMON`/`USERS`) verbs. The protocol surface is
408
472
  feature-complete for modern clients.
473
+ - **Phase 12 — IRCv3 extension sweep.** ✅ landed in **v0.6.0**: ten
474
+ negotiated caps (`account-notify`, `msgid`, `standard-replies`,
475
+ `MONITOR`, `labeled-response`, `sts`, `draft/typing`,
476
+ `draft/multiline`, `draft/read-marker`, `draft/pre-away`), the
477
+ ISUPPORT tokens for the new caps (`MONITOR`/`MULTILINE`/`TYPING`/
478
+ `STATUSMSG`/`EXTBAN`/`ACCOUNTEXTBAN`), the read-only user mode `S`
479
+ (TLS connected), and the async push fanout seam (Registry hooks →
480
+ `MONITOR` online/offline, `BroadcastWallops`-style caps-gated
481
+ delivery).
482
+ - **Phase 13 — IRC services.** ⬳ pending: `ServicesStore` port +
483
+ NickServ / ChanServ / HostServ / OperServ / MemoServ. Adapter
484
+ backends (D1 / DynamoDB) ship alongside; until then
485
+ `draft/read-marker` and `draft/pre-away` use in-memory reference
486
+ stores and do not survive an adapter restart.
487
+ - **Phase 14 — Web client.** ⬳ pending: vendored Kiwi IRC SPA served
488
+ from the Worker, WS Origin allowlist (CSWSH defense), landing page,
489
+ headless browser e2e.
490
+ - **Coverage hardening.** 🔄 partial as of **v0.6.0**: `aws-adapter`
491
+ and `aws-stack` now clear the 90% gate (CI boots DynamoDB Local via
492
+ a JRE; the CDK synth no longer re-bundles under coverage), and the
493
+ CF packages moved to istanbul to clear the v0.5.0 workerd-incompat.
494
+ Follow-ups drive each remaining sub-100% package to 100%.
409
495
 
410
496
  ---
411
497
 
@@ -416,6 +502,8 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
416
502
  channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`,
417
503
  `WHOWAS`, `MOTD`, `AWAY`, `OPER` (credential auth → `o` user mode), the
418
504
  query verbs `VERSION`, `TIME`, `ADMIN`, `INFO`, `USERHOST`, `ISON`, the
505
+ modern-online-presence verb `MONITOR` (`+`/`-`/`C`/`L`/`S` with the
506
+ `730`/`731`/`732`/`734` numerics and async online/offline push), the
419
507
  network-statistics verbs `LUSERS` and `STATS` (`u` uptime + `l` link-info
420
508
  letters; unknown letters get the charybdis-style empty body), the
421
509
  routing-diagnostic verb `TRACE` (oper-gated per-connection detail), the
@@ -429,15 +517,31 @@ v0.5.0 — they return `421 ERR_UNKNOWNCOMMAND`, and their reserved
429
517
  numerics have been removed. S2S linking is a PLAN non-goal.)
430
518
 
431
519
  **Channel modes:** `o v b i k l t n m s p`.
432
- **User modes:** `i`, `o` (local only), `w`, `s`.
520
+ **User modes:** `i`, `o` (local only), `w`, `s`, and the read-only `S`
521
+ (TLS connected — set by the transport at registration, surfaced in WHOIS
522
+ via `276 RPL_WHOISSECURE`, not settable via `MODE`).
433
523
 
434
524
  **IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
435
- `TAGMSG` command), `server-time`, `account-tag`, `echo-message`, `batch`,
525
+ `TAGMSG` command), `server-time`, `account-tag`, `account-notify`
526
+ (pushes `ACCOUNT` on SASL login/logout), `echo-message`, `batch`,
436
527
  `sasl` (`PLAIN` always; `EXTERNAL` via mTLS when a client-cert trust store
437
- is bound),
438
- `multi-prefix`, `away-notify`, `chghost`, `invite-notify`, `extended-join`,
439
- `draft/chathistory`, `safelist`. Case-insensitive nick/channel comparison
440
- uses **RFC 1459** case-mapping (advertised via `005 CASEMAPPING=rfc1459`).
528
+ is bound), `multi-prefix`, `away-notify`, `chghost`, `invite-notify`,
529
+ `extended-join`, `msgid` (`@+msgid=<id>` on every PRIVMSG/NOTICE/TAGMSG,
530
+ shared between live and `draft/chathistory` replay), `standard-replies`
531
+ (`FAIL`/`WARN`/`NOTE` replacements for a curated numeric subset),
532
+ `MONITOR=<n>` (config-driven ceiling, default 30), `labeled-response`
533
+ (`+label=<id>` → `BATCH +id labeled-response … BATCH -id` wrapping),
534
+ `sts` (Strict Transport Security — conditionally advertised with
535
+ `duration`/`port`/optional `preload` from `ServerConfig` when configured,
536
+ so compliant clients upgrade from plaintext to TLS and pin the secure
537
+ listener), `draft/chathistory`, `safelist`, `draft/typing` (typing-indicator
538
+ broadcast), `draft/multiline=<n>` (multi-line `BATCH`, default 4096-byte
539
+ budget), `draft/read-marker` (per-account persisted last-read), and
540
+ `draft/pre-away` (per-account persisted away reason, replayed at
541
+ identify). Case-insensitive nick/channel comparison uses **RFC 1459**
542
+ case-mapping (advertised via `005 CASEMAPPING=rfc1459`). The new
543
+ `MONITOR` / `MULTILINE` / `TYPING` / `STATUSMSG` / `EXTBAN` /
544
+ `ACCOUNTEXTBAN` ISUPPORT tokens are advertised from `ServerConfig`.
441
545
 
442
546
  **Transport:** deployed stacks (Cloudflare Workers, AWS API Gateway) speak
443
547
  WebSocket text frames by default (one IRC message per frame, with tolerance
@@ -457,9 +561,15 @@ bridges a stock TCP client to a deployed WebSocket endpoint.
457
561
  gates), v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
458
562
  ChannelRegistryDO, SASL account persistence), v0.4.0 (dual transport
459
563
  `irc+tls :6697`, SASL `EXTERNAL`/mTLS, WHOWAS + remaining verbs, D1 account
460
- store, Node 24/pnpm 11), and v0.5.0 (deferred IRC verbs `KILL` / `REHASH`
564
+ store, Node 24/pnpm 11), v0.5.0 (deferred IRC verbs `KILL` / `REHASH`
461
565
  / `LUSERS` + `STATS` / `TRACE` / `WALLOPS` / `SETNAME`, plus the formal
462
- drop of the S2S and obsolete RFC 2812 verbs).
566
+ drop of the S2S and obsolete RFC 2812 verbs), and **v0.6.0** (the IRCv3
567
+ extension sweep — `account-notify`, `msgid`, `standard-replies`,
568
+ `MONITOR`, `labeled-response`, `sts`, `draft/typing`, `draft/multiline`,
569
+ `draft/read-marker`, `draft/pre-away`, plus user mode `S`; the
570
+ required-`serverName` gate; `tools/load-test`; aws-adapter / aws-stack
571
+ coverage unblocks; CF packages moved to istanbul; deploys now
572
+ manual-only).
463
573
  - `docs/Cloudflare-TCP-Deployment.md` and `docs/AWS-TCP-Deployment.md` —
464
574
  end-to-end guides for the `:6697` TCP+TLS variants (Spectrum/Container on
465
575
  CF, NLB + Lambda streaming on AWS), including mTLS trust-store setup.
@@ -60,6 +60,39 @@ export AWS_DEFAULT_REGION=us-east-1
60
60
  LOCALSTACK=1 pnpm test
61
61
  ```
62
62
 
63
+ ## Test-mode bundle stub (why coverage does not re-bundle)
64
+
65
+ The stack ships four `NodejsFunction` constructs (the wss `IrcHandler`,
66
+ `IrcSweeper`, `IrcPingChecker`, and the optional NLB `IrcNlbHandler`).
67
+ Each one invokes esbuild at synth time to bundle the shared handler
68
+ entry. Repeated across the ~80 synth assertions in `tests/stack.test.ts`,
69
+ that meant the bundler ran **dozens to hundreds of times per coverage
70
+ invocation**, exhausting CI memory before vitest could print its
71
+ coverage table — the failure mode presented as a wall of
72
+ `Bundling asset TestStack/IrcHandler/Code/Stage…` lines followed by
73
+ `[ELIFECYCLE] Command failed with exit code N` and no report.
74
+
75
+ The fix is a single env-var toggle consumed by `makeLambda()` in
76
+ `src/aws-stack.ts`:
77
+
78
+ | Env var | Effect on the synth |
79
+ |-----------------------------------|-----------------------------------------------------------------------|
80
+ | `IRC_AWS_STACK_TEST_NO_BUNDLE=1` | Every Lambda is emitted as an inline `Code.fromInline('void 0')` zip; esbuild is not invoked at all. CloudFormation shape (runtime, env vars, IAM grants, route wiring, …) is identical to the production synth. |
81
+ | _(unset)_ | Production path: every Lambda is a real `NodejsFunction` whose asset is esbuild-bundled at synth time. This is what `cdk synth` / `cdk deploy` run. |
82
+
83
+ `tests/vitest.setup.ts` sets the flag once per test file so the entire
84
+ `pnpm test` / `pnpm coverage` run stays on the stub path. Production
85
+ synth (`bin/aws.ts` via the CDK CLI) never loads vitest, so it cannot
86
+ accidentally inherit the flag.
87
+
88
+ A small `tests/synth-no-bundle.test.ts` locks both paths in place: it
89
+ asserts the stub emits `Code: { ZipFile }` when the flag is set, and
90
+ exercises the production `NodejsFunction` return once (with the flag
91
+ explicitly unset) so the production branch stays covered. The
92
+ end-to-end guard lives in `tests/coverage-command.test.ts`, which
93
+ spawns `pnpm --filter @serverless-ircd/aws-stack coverage` in a
94
+ subprocess and asserts the report prints an `All files` row.
95
+
63
96
  ## Stack outputs
64
97
 
65
98
  | Output | Description |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/aws-stack",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",