serverless-ircd 0.1.0 → 0.2.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 (198) hide show
  1. package/.github/workflows/ci.yml +96 -2
  2. package/.github/workflows/deploy-aws.yml +129 -0
  3. package/.github/workflows/deploy-cf.yml +0 -2
  4. package/.gitmodules +3 -0
  5. package/AGENTS.md +5 -0
  6. package/CHANGELOG.md +352 -0
  7. package/MOTD.txt +3 -0
  8. package/PLAN-FIXES.md +358 -0
  9. package/PLAN.md +420 -0
  10. package/README.md +115 -81
  11. package/apps/aws-stack/README.md +73 -0
  12. package/apps/aws-stack/bin/aws.ts +49 -0
  13. package/apps/aws-stack/cdk.json +10 -0
  14. package/apps/aws-stack/package.json +41 -0
  15. package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
  16. package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
  17. package/apps/aws-stack/scripts/smoke.mjs +142 -0
  18. package/apps/aws-stack/src/aws-stack.ts +263 -0
  19. package/apps/aws-stack/src/tables.ts +10 -0
  20. package/apps/aws-stack/tests/localstack.test.ts +46 -0
  21. package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
  22. package/apps/aws-stack/tests/stack.test.ts +464 -0
  23. package/apps/aws-stack/tsconfig.build.json +11 -0
  24. package/apps/aws-stack/tsconfig.test.json +8 -0
  25. package/apps/aws-stack/vitest.config.ts +20 -0
  26. package/apps/cf-worker/package.json +1 -1
  27. package/apps/cf-worker/wrangler.toml +25 -0
  28. package/apps/local-cli/package.json +1 -1
  29. package/apps/local-cli/src/config-loader.ts +56 -0
  30. package/apps/local-cli/src/server.ts +241 -32
  31. package/apps/local-cli/tests/config-loader.test.ts +107 -0
  32. package/apps/local-cli/tests/e2e.test.ts +74 -0
  33. package/apps/local-cli/tests/security-e2e.test.ts +239 -0
  34. package/biome.json +3 -1
  35. package/dashboards/cloudwatch-irc.json +139 -0
  36. package/docs/AWS-Adapter-Architecture.md +494 -0
  37. package/docs/AWS-Deployment.md +1107 -0
  38. package/docs/Cloudflare-Deployment-Guide.md +660 -0
  39. package/docs/Home.md +1 -0
  40. package/docs/Observability.md +87 -0
  41. package/docs/PlanIRCv3Websocket.md +489 -0
  42. package/docs/PlanWebClient.md +451 -0
  43. package/docs/Release-Process.md +440 -0
  44. package/package.json +8 -2
  45. package/packages/aws-adapter/README.md +35 -0
  46. package/packages/aws-adapter/package.json +52 -0
  47. package/packages/aws-adapter/src/aws-runtime.ts +783 -0
  48. package/packages/aws-adapter/src/cdk-table-defs.ts +69 -0
  49. package/packages/aws-adapter/src/config-loader.ts +96 -0
  50. package/packages/aws-adapter/src/dynamo.ts +61 -0
  51. package/packages/aws-adapter/src/handlers/connect.ts +44 -0
  52. package/packages/aws-adapter/src/handlers/default.ts +322 -0
  53. package/packages/aws-adapter/src/handlers/disconnect.ts +29 -0
  54. package/packages/aws-adapter/src/handlers/index.ts +248 -0
  55. package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
  56. package/packages/aws-adapter/src/handlers/state.ts +13 -0
  57. package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
  58. package/packages/aws-adapter/src/index.ts +67 -0
  59. package/packages/aws-adapter/src/message-store.ts +34 -0
  60. package/packages/aws-adapter/src/serialize.ts +283 -0
  61. package/packages/aws-adapter/src/tables.ts +53 -0
  62. package/packages/aws-adapter/tests/aws-harness.ts +408 -0
  63. package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
  64. package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
  65. package/packages/aws-adapter/tests/config-loader.test.ts +158 -0
  66. package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
  67. package/packages/aws-adapter/tests/global-setup.ts +301 -0
  68. package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
  69. package/packages/aws-adapter/tests/handlers.test.ts +427 -0
  70. package/packages/aws-adapter/tests/message-store.test.ts +55 -0
  71. package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
  72. package/packages/aws-adapter/tests/serialize.test.ts +249 -0
  73. package/packages/aws-adapter/tests/smoke.test.ts +48 -0
  74. package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
  75. package/packages/aws-adapter/tests/tables.test.ts +74 -0
  76. package/packages/aws-adapter/tests/transactions.test.ts +446 -0
  77. package/packages/aws-adapter/tsconfig.build.json +16 -0
  78. package/packages/aws-adapter/tsconfig.test.json +14 -0
  79. package/packages/aws-adapter/vitest.config.ts +23 -0
  80. package/packages/cf-adapter/package.json +2 -1
  81. package/packages/cf-adapter/src/config-loader.ts +106 -0
  82. package/packages/cf-adapter/src/connection-do.ts +34 -0
  83. package/packages/cf-adapter/tests/cf-harness.ts +2 -2
  84. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
  85. package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
  86. package/packages/cf-adapter/tests/connection-do.test.ts +42 -0
  87. package/packages/in-memory-runtime/package.json +1 -1
  88. package/packages/in-memory-runtime/src/in-memory-runtime.ts +108 -3
  89. package/packages/in-memory-runtime/src/index.ts +1 -1
  90. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +115 -0
  91. package/packages/irc-core/package.json +12 -2
  92. package/packages/irc-core/src/admission.ts +216 -0
  93. package/packages/irc-core/src/caps/capabilities.ts +1 -0
  94. package/packages/irc-core/src/cloak.ts +81 -0
  95. package/packages/irc-core/src/commands/cap.ts +1 -2
  96. package/packages/irc-core/src/commands/chathistory.ts +305 -0
  97. package/packages/irc-core/src/commands/index.ts +7 -0
  98. package/packages/irc-core/src/commands/join.ts +59 -7
  99. package/packages/irc-core/src/commands/privmsg.ts +24 -0
  100. package/packages/irc-core/src/commands/registration.ts +71 -14
  101. package/packages/irc-core/src/commands/sasl.ts +251 -0
  102. package/packages/irc-core/src/config.ts +247 -0
  103. package/packages/irc-core/src/flood-control.ts +175 -0
  104. package/packages/irc-core/src/index.ts +5 -0
  105. package/packages/irc-core/src/ports.ts +655 -0
  106. package/packages/irc-core/src/protocol/base64.ts +16 -0
  107. package/packages/irc-core/src/protocol/index.ts +2 -1
  108. package/packages/irc-core/src/protocol/numerics.ts +11 -0
  109. package/packages/irc-core/src/protocol/parser.ts +27 -2
  110. package/packages/irc-core/src/state/connection.ts +41 -0
  111. package/packages/irc-core/src/types.ts +66 -2
  112. package/packages/irc-core/stryker.commands.conf.json +41 -0
  113. package/packages/irc-core/stryker.protocol.conf.json +26 -0
  114. package/packages/irc-core/tests/admission.test.ts +229 -0
  115. package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
  116. package/packages/irc-core/tests/cloak.test.ts +78 -0
  117. package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
  118. package/packages/irc-core/tests/commands/join.test.ts +246 -2
  119. package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
  120. package/packages/irc-core/tests/commands/registration.test.ts +277 -9
  121. package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
  122. package/packages/irc-core/tests/config.test.ts +646 -0
  123. package/packages/irc-core/tests/flood-control.test.ts +394 -0
  124. package/packages/irc-core/tests/message-store.test.ts +530 -0
  125. package/packages/irc-core/tests/parser.test.ts +44 -1
  126. package/packages/irc-core/tests/ports.test.ts +263 -0
  127. package/packages/irc-server/package.json +1 -1
  128. package/packages/irc-server/src/actor.ts +162 -44
  129. package/packages/irc-server/src/dispatch.ts +89 -5
  130. package/packages/irc-server/src/index.ts +2 -0
  131. package/packages/irc-server/src/routing.ts +151 -0
  132. package/packages/irc-server/tests/actor.test.ts +470 -14
  133. package/packages/irc-server/tests/dispatch.test.ts +84 -0
  134. package/packages/irc-server/tests/routing.test.ts +201 -0
  135. package/packages/irc-test-support/README.md +32 -0
  136. package/packages/irc-test-support/package.json +37 -0
  137. package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +1 -1
  138. package/packages/irc-test-support/src/index.ts +24 -0
  139. package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
  140. package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
  141. package/packages/irc-test-support/tests/smoke.test.ts +11 -0
  142. package/packages/irc-test-support/tsconfig.build.json +16 -0
  143. package/packages/irc-test-support/tsconfig.test.json +14 -0
  144. package/packages/irc-test-support/vitest.config.ts +20 -0
  145. package/progress.md +107 -0
  146. package/tickets.md +2485 -0
  147. package/tools/ci-hardening/package.json +30 -0
  148. package/tools/ci-hardening/src/index.ts +6 -0
  149. package/tools/ci-hardening/src/validate.ts +103 -0
  150. package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
  151. package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
  152. package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
  153. package/tools/ci-hardening/tests/validate.test.ts +177 -0
  154. package/tools/ci-hardening/tsconfig.build.json +12 -0
  155. package/tools/ci-hardening/tsconfig.test.json +10 -0
  156. package/tools/ci-hardening/vitest.config.ts +21 -0
  157. package/tools/tcp-ws-forwarder/package.json +1 -1
  158. package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
  159. package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
  160. package/tools/tcp-ws-forwarder/src/main.ts +33 -14
  161. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
  162. package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
  163. package/webircgateway/LICENSE +201 -0
  164. package/webircgateway/Makefile +44 -0
  165. package/webircgateway/README.md +134 -0
  166. package/webircgateway/config.conf.example +135 -0
  167. package/webircgateway/go.mod +16 -0
  168. package/webircgateway/go.sum +89 -0
  169. package/webircgateway/main.go +118 -0
  170. package/webircgateway/pkg/dnsbl/dnsbl.go +121 -0
  171. package/webircgateway/pkg/identd/identd.go +86 -0
  172. package/webircgateway/pkg/identd/rpcclient.go +59 -0
  173. package/webircgateway/pkg/irc/isupport.go +56 -0
  174. package/webircgateway/pkg/irc/message.go +217 -0
  175. package/webircgateway/pkg/irc/state.go +79 -0
  176. package/webircgateway/pkg/proxy/proxy.go +129 -0
  177. package/webircgateway/pkg/proxy/server.go +237 -0
  178. package/webircgateway/pkg/recaptcha/recaptcha.go +59 -0
  179. package/webircgateway/pkg/webircgateway/client.go +741 -0
  180. package/webircgateway/pkg/webircgateway/client_command_handlers.go +495 -0
  181. package/webircgateway/pkg/webircgateway/config.go +385 -0
  182. package/webircgateway/pkg/webircgateway/gateway.go +278 -0
  183. package/webircgateway/pkg/webircgateway/gateway_utils.go +133 -0
  184. package/webircgateway/pkg/webircgateway/hooks.go +152 -0
  185. package/webircgateway/pkg/webircgateway/letsencrypt.go +41 -0
  186. package/webircgateway/pkg/webircgateway/messagetags.go +103 -0
  187. package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +206 -0
  188. package/webircgateway/pkg/webircgateway/transport_sockjs.go +107 -0
  189. package/webircgateway/pkg/webircgateway/transport_tcp.go +113 -0
  190. package/webircgateway/pkg/webircgateway/transport_websocket.go +126 -0
  191. package/webircgateway/pkg/webircgateway/utils.go +147 -0
  192. package/webircgateway/plugins/example/plugin.go +11 -0
  193. package/webircgateway/plugins/stats/plugin.go +52 -0
  194. package/webircgateway/staticcheck.conf +1 -0
  195. package/webircgateway/webircgateway.svg +3 -0
  196. package/packages/cf-adapter/tests/integration/index.ts +0 -17
  197. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
  198. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
@@ -9,9 +9,16 @@
9
9
  * - Consecutive identical Send effects coalesce into one runtime call
10
10
  * (the test named "dedup").
11
11
  * - The first runtime rejection propagates and stops processing.
12
+ *
13
+ * When an optional {@link DispatchOptions.logger} is supplied, dispatch
14
+ * emits a `dispatch` debug record describing the effect count and a
15
+ * `dispatch.error` warn record when a handler rejects — both stamped
16
+ * with the caller's `traceId`/`connectionId` so the actor → runtime
17
+ * pipeline can be reconstructed end-to-end.
12
18
  */
13
19
 
14
- import type { BroadcastEffect, Effect, RawLine } from '@serverless-ircd/irc-core';
20
+ import type { BroadcastEffect, Effect, Logger, RawLine } from '@serverless-ircd/irc-core';
21
+ import { LogLevel } from '@serverless-ircd/irc-core';
15
22
  import type { IrcRuntime } from './runtime.js';
16
23
 
17
24
  /** Discriminated-union handler: each Effect tag maps to one runtime call. */
@@ -33,6 +40,29 @@ const handlers: { [K in Effect['tag']]: Handler<Extract<Effect, { tag: K }>> } =
33
40
 
34
41
  function noop(): void {}
35
42
 
43
+ /**
44
+ * Optional knobs for {@link dispatch}. All observability-related; the
45
+ * core effect-interpretation behaviour is unchanged when they are absent.
46
+ */
47
+ export interface DispatchOptions {
48
+ /**
49
+ * Logger to emit `dispatch` / `dispatch.error` records to. When omitted,
50
+ * dispatch is silent — preserving the default silent contract.
51
+ */
52
+ readonly logger?: Logger;
53
+ /**
54
+ * Trace id to stamp on every log record. When omitted, records carry
55
+ * `traceId: undefined` (loggers drop undefined fields, so the field is
56
+ * absent from the JSON line).
57
+ */
58
+ readonly traceId?: string;
59
+ /**
60
+ * Connection id the dispatch is serving (the actor's bound connId).
61
+ * Stamped on every record alongside `traceId`.
62
+ */
63
+ readonly connectionId?: string;
64
+ }
65
+
36
66
  /**
37
67
  * Returns true iff two effects are interchangeable — used to coalesce
38
68
  * consecutive duplicates so a reducer's accidental double-send is a no-op.
@@ -61,24 +91,78 @@ function linesEqual(a: RawLine[], b: RawLine[]): boolean {
61
91
  /**
62
92
  * Interprets `effects` in order against `runtime`. Returns when every
63
93
  * effect has been performed; the first rejection propagates.
94
+ *
95
+ * When `opts.logger` is supplied a `dispatch` debug record is emitted
96
+ * before the loop begins (with the effect count and a per-tag histogram).
64
97
  */
65
- export async function dispatch(effects: Effect[], runtime: IrcRuntime): Promise<void> {
98
+ export async function dispatch(
99
+ effects: Effect[],
100
+ runtime: IrcRuntime,
101
+ opts?: DispatchOptions,
102
+ ): Promise<void> {
103
+ if (opts?.logger !== undefined) {
104
+ logDispatch(opts.logger, opts.traceId, opts.connectionId, effects);
105
+ }
66
106
  let previous: Effect | undefined;
67
107
  for (const effect of effects) {
68
108
  if (previous !== undefined && isSameSend(previous, effect)) {
69
109
  previous = effect;
70
110
  continue;
71
111
  }
72
- await handle(effect, runtime);
112
+ await handle(effect, runtime, opts);
73
113
  previous = effect;
74
114
  }
75
115
  }
76
116
 
77
- async function handle(effect: Effect, runtime: IrcRuntime): Promise<void> {
117
+ async function handle(effect: Effect, runtime: IrcRuntime, opts?: DispatchOptions): Promise<void> {
78
118
  // The dispatch table is keyed by tag; the cast is the standard TS pattern
79
119
  // for exhaustive-union dispatch.
80
120
  const handler = handlers[effect.tag] as Handler<Effect>;
81
- await handler(effect, runtime);
121
+ try {
122
+ await handler(effect, runtime);
123
+ } catch (err) {
124
+ if (opts?.logger !== undefined) {
125
+ opts.logger.on(logLevelFor(err), 'dispatch.error', {
126
+ traceId: opts.traceId,
127
+ connectionId: opts.connectionId,
128
+ effect: effect.tag,
129
+ err: err instanceof Error ? { name: err.name, message: err.message } : String(err),
130
+ });
131
+ }
132
+ throw err;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Emits one `dispatch` debug record per call with a per-tag histogram so
138
+ * dashboards can slice "broadcasts per frame", "sends per frame", etc.
139
+ */
140
+ function logDispatch(
141
+ logger: Logger,
142
+ traceId: string | undefined,
143
+ connectionId: string | undefined,
144
+ effects: Effect[],
145
+ ): void {
146
+ const histogram: Record<string, number> = {};
147
+ for (const e of effects) {
148
+ histogram[e.tag] = (histogram[e.tag] ?? 0) + 1;
149
+ }
150
+ logger.on(LogLevel.Debug, 'dispatch', {
151
+ traceId,
152
+ connectionId,
153
+ effects: effects.map((e) => e.tag),
154
+ histogram,
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Maps a thrown error to the level dispatch should log it at. Today every
160
+ * dispatch rejection is operational (transient network / DO fetch issues),
161
+ * so warn is appropriate; handlers do not throw for input-shape problems
162
+ * (those surface as IRC numerics via Send effects).
163
+ */
164
+ function logLevelFor(_err: unknown): LogLevel {
165
+ return LogLevel.Warn;
82
166
  }
83
167
 
84
168
  /**
@@ -9,3 +9,5 @@ export type { IrcRuntime } from './runtime.js';
9
9
  export { dispatch } from './dispatch.js';
10
10
  export { ConnectionActor, splitFrameLines } from './actor.js';
11
11
  export type { ActorChannelAccess, ConnectionActorOptions } from './actor.js';
12
+ export { buildRoutedReducers, resolveFloodControl } from './routing.js';
13
+ export type { RoutedReducers } from './routing.js';
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Routed reducer map construction for the {@link ConnectionActor}.
3
+ *
4
+ * Every command the actor dispatches runs through one of the reducers in
5
+ * this map. {@link buildRoutedReducers} wraps each reducer with the
6
+ * per-connection flood-control token bucket so a flooding client is
7
+ * disconnected end-to-end regardless of whether the routed command mutates
8
+ * connection state (PING/USER/…) or channel state (JOIN/PRIVMSG/…). Flood
9
+ * control bounds bytes-in from the client, not which authority owns the
10
+ * state the command touches.
11
+ *
12
+ * The wrapper reads `ctx.clock` (the actor's injected {@link Clock}) and
13
+ * stores the bucket on `ctx.connection.floodBucket`, so the same time source
14
+ * the rest of the actor uses drives refill. Extracted as a pure helper so the
15
+ * actor and unit tests can inspect the wrapped map without spinning up a full
16
+ * actor + fake runtime.
17
+ *
18
+ * Three reducers (`listReducer`, `whoReducer`, `whoisReducer`) take a bespoke
19
+ * signature that fetches runtime state before invocation; they do not fit the
20
+ * `Reducer<S>` shape the flood wrapper operates on and are therefore left
21
+ * unwrapped here. They are read-only query paths; the per-connection bucket
22
+ * still bounds the write-path commands a client can issue around them.
23
+ */
24
+
25
+ import {
26
+ type ChannelState,
27
+ type ConnectionState,
28
+ DEFAULT_FLOOD_CONTROL_CONFIG,
29
+ type FloodControlConfig,
30
+ type Reducer,
31
+ type ServerConfig,
32
+ authenticateReducer,
33
+ awayReducer,
34
+ capReducer,
35
+ channelModeReducer,
36
+ defaultCommandCost,
37
+ inviteReducer,
38
+ joinReducer,
39
+ kickReducer,
40
+ motdReducer,
41
+ namesReducer,
42
+ nickReducer,
43
+ noticeChannelReducer,
44
+ noticeUserReducer,
45
+ partReducer,
46
+ passReducer,
47
+ pingReducer,
48
+ pongReducer,
49
+ privmsgChannelReducer,
50
+ privmsgUserReducer,
51
+ quitReducer,
52
+ topicReducer,
53
+ userModeReducer,
54
+ userReducer,
55
+ wrapWithFloodControl,
56
+ } from '@serverless-ircd/irc-core';
57
+
58
+ /**
59
+ * The wrapped reducer for every command the actor routes by name. Keyed by
60
+ * logical command rather than IRC verb because PRIVMSG/NOTICE/MODE each fan
61
+ * out to two reducers depending on the target shape (channel vs. user).
62
+ */
63
+ export interface RoutedReducers {
64
+ // Connection-authority reducers.
65
+ readonly ping: Reducer<ConnectionState>;
66
+ readonly pong: Reducer<ConnectionState>;
67
+ readonly nick: Reducer<ConnectionState>;
68
+ readonly user: Reducer<ConnectionState>;
69
+ readonly pass: Reducer<ConnectionState>;
70
+ readonly quit: Reducer<ConnectionState>;
71
+ readonly motd: Reducer<ConnectionState>;
72
+ readonly cap: Reducer<ConnectionState>;
73
+ readonly authenticate: Reducer<ConnectionState>;
74
+ readonly away: Reducer<ConnectionState>;
75
+ // Channel-authority reducers.
76
+ readonly join: Reducer<ChannelState>;
77
+ readonly part: Reducer<ChannelState>;
78
+ readonly topic: Reducer<ChannelState>;
79
+ readonly names: Reducer<ChannelState>;
80
+ readonly kick: Reducer<ChannelState>;
81
+ readonly invite: Reducer<ChannelState>;
82
+ // Target-dependent reducers.
83
+ readonly privmsgChannel: Reducer<ChannelState>;
84
+ readonly privmsgUser: Reducer<ConnectionState>;
85
+ readonly noticeChannel: Reducer<ChannelState>;
86
+ readonly noticeUser: Reducer<ConnectionState>;
87
+ readonly channelMode: Reducer<ChannelState>;
88
+ readonly userMode: Reducer<ConnectionState>;
89
+ }
90
+
91
+ /**
92
+ * Resolves the effective flood-control config for a deployment.
93
+ *
94
+ * - `serverConfig.floodControl === null` → returns `null` (disabled; the
95
+ * actor skips wrapping entirely).
96
+ * - `undefined` → the documented {@link DEFAULT_FLOOD_CONTROL_CONFIG}.
97
+ * - an object → those thresholds.
98
+ *
99
+ * Control-plane commands (`PING`/`PONG`/`CAP`/`AUTHENTICATE`/`QUIT`) are
100
+ * exempt by default via {@link defaultCommandCost}. A caller-supplied
101
+ * `commandCost` (only possible on a code-constructed config — it cannot be
102
+ * serialized) is preserved as-is.
103
+ */
104
+ export function resolveFloodControl(serverConfig: ServerConfig): FloodControlConfig | null {
105
+ const fc = serverConfig.floodControl;
106
+ if (fc === null) return null;
107
+ if (fc === undefined) {
108
+ return { ...DEFAULT_FLOOD_CONTROL_CONFIG, commandCost: defaultCommandCost };
109
+ }
110
+ return { ...fc, commandCost: fc.commandCost ?? defaultCommandCost };
111
+ }
112
+
113
+ /**
114
+ * Builds the wrapped reducer map for a connection.
115
+ *
116
+ * When flood control is disabled (`floodControl: null`), returns the raw
117
+ * reducers unchanged so existing behaviour is preserved bit-for-bit.
118
+ * Otherwise every reducer is wrapped with {@link wrapWithFloodControl} using
119
+ * the resolved config, so the token-bucket check runs on every dispatched
120
+ * command.
121
+ */
122
+ export function buildRoutedReducers(serverConfig: ServerConfig): RoutedReducers {
123
+ const floodControl = resolveFloodControl(serverConfig);
124
+ const wrap = <S>(reducer: Reducer<S>): Reducer<S> =>
125
+ floodControl === null ? reducer : wrapWithFloodControl(reducer, floodControl);
126
+
127
+ return {
128
+ ping: wrap(pingReducer),
129
+ pong: wrap(pongReducer),
130
+ nick: wrap(nickReducer),
131
+ user: wrap(userReducer),
132
+ pass: wrap(passReducer),
133
+ quit: wrap(quitReducer),
134
+ motd: wrap(motdReducer),
135
+ cap: wrap(capReducer),
136
+ authenticate: wrap(authenticateReducer),
137
+ away: wrap(awayReducer),
138
+ join: wrap(joinReducer),
139
+ part: wrap(partReducer),
140
+ topic: wrap(topicReducer),
141
+ names: wrap(namesReducer),
142
+ kick: wrap(kickReducer),
143
+ invite: wrap(inviteReducer),
144
+ privmsgChannel: wrap(privmsgChannelReducer),
145
+ privmsgUser: wrap(privmsgUserReducer),
146
+ noticeChannel: wrap(noticeChannelReducer),
147
+ noticeUser: wrap(noticeUserReducer),
148
+ channelMode: wrap(channelModeReducer),
149
+ userMode: wrap(userModeReducer),
150
+ };
151
+ }