serverless-ircd 0.4.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 (243) 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 +435 -0
  6. package/README.md +206 -27
  7. package/apps/aws-stack/README.md +37 -3
  8. package/apps/aws-stack/package.json +1 -1
  9. package/apps/aws-stack/src/aws-stack.ts +106 -13
  10. package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
  11. package/apps/aws-stack/tests/stack.test.ts +49 -3
  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 +138 -28
  27. package/apps/local-cli/tests/e2e.test.ts +113 -29
  28. package/apps/local-cli/tests/tcp.test.ts +71 -19
  29. package/apps/local-cli/tests/ws-subprotocol.test.ts +262 -0
  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/aws-runtime.ts +69 -0
  55. package/packages/aws-adapter/src/handlers/connect.ts +36 -5
  56. package/packages/aws-adapter/src/handlers/default.ts +66 -5
  57. package/packages/aws-adapter/src/handlers/index.ts +41 -2
  58. package/packages/aws-adapter/src/handlers/nlb-stream.ts +18 -0
  59. package/packages/aws-adapter/src/index.ts +2 -0
  60. package/packages/aws-adapter/src/serialize.ts +40 -2
  61. package/packages/aws-adapter/src/stats.ts +80 -0
  62. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
  63. package/packages/aws-adapter/tests/account-store.test.ts +4 -7
  64. package/packages/aws-adapter/tests/aws-harness.ts +5 -2
  65. package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
  66. package/packages/aws-adapter/tests/aws-runtime.test.ts +140 -0
  67. package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
  68. package/packages/aws-adapter/tests/connect.test.ts +100 -4
  69. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
  70. package/packages/aws-adapter/tests/global-setup.ts +55 -30
  71. package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
  72. package/packages/aws-adapter/tests/handlers.test.ts +238 -4
  73. package/packages/aws-adapter/tests/message-store.test.ts +6 -5
  74. package/packages/aws-adapter/tests/nlb-stream.test.ts +62 -8
  75. package/packages/aws-adapter/tests/stats.test.ts +317 -0
  76. package/packages/cf-adapter/package.json +6 -1
  77. package/packages/cf-adapter/src/cf-runtime.ts +66 -1
  78. package/packages/cf-adapter/src/channel-do.ts +2 -2
  79. package/packages/cf-adapter/src/connection-do.ts +185 -54
  80. package/packages/cf-adapter/src/env.ts +25 -6
  81. package/packages/cf-adapter/src/index.ts +2 -0
  82. package/packages/cf-adapter/src/registry-do.ts +22 -3
  83. package/packages/cf-adapter/src/serialize.ts +25 -4
  84. package/packages/cf-adapter/src/sharding.ts +1 -2
  85. package/packages/cf-adapter/src/stats.ts +65 -0
  86. package/packages/cf-adapter/tests/cf-harness.ts +1 -1
  87. package/packages/cf-adapter/tests/cf-integration.test.ts +4 -4
  88. package/packages/cf-adapter/tests/cf-runtime.test.ts +307 -2
  89. package/packages/cf-adapter/tests/channel-do.test.ts +119 -2
  90. package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
  91. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +2 -2
  92. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +2 -2
  93. package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
  94. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
  95. package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
  96. package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
  97. package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
  98. package/packages/cf-adapter/tests/registry-do.test.ts +108 -4
  99. package/packages/cf-adapter/tests/serialize.test.ts +308 -0
  100. package/packages/cf-adapter/tests/sharding.test.ts +1 -1
  101. package/packages/cf-adapter/tests/stats.test.ts +120 -0
  102. package/packages/cf-adapter/tests/worker/main.ts +7 -7
  103. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
  104. package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
  105. package/packages/cf-adapter/vitest.config.ts +1 -1
  106. package/packages/cf-adapter/wrangler.test.toml +7 -0
  107. package/packages/in-memory-runtime/package.json +1 -1
  108. package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
  109. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
  110. package/packages/irc-core/package.json +1 -1
  111. package/packages/irc-core/src/admission.ts +16 -15
  112. package/packages/irc-core/src/caps/capabilities.ts +38 -4
  113. package/packages/irc-core/src/caps/index.ts +1 -0
  114. package/packages/irc-core/src/caps/sts.ts +84 -0
  115. package/packages/irc-core/src/commands/account-notify.ts +53 -0
  116. package/packages/irc-core/src/commands/away.ts +9 -3
  117. package/packages/irc-core/src/commands/cap.ts +23 -5
  118. package/packages/irc-core/src/commands/chathistory.ts +40 -11
  119. package/packages/irc-core/src/commands/index.ts +42 -0
  120. package/packages/irc-core/src/commands/invite.ts +2 -4
  121. package/packages/irc-core/src/commands/isupport.ts +59 -2
  122. package/packages/irc-core/src/commands/kick.ts +2 -4
  123. package/packages/irc-core/src/commands/kill.ts +127 -0
  124. package/packages/irc-core/src/commands/list.ts +1 -1
  125. package/packages/irc-core/src/commands/lusers.ts +204 -0
  126. package/packages/irc-core/src/commands/mode.ts +12 -9
  127. package/packages/irc-core/src/commands/monitor.ts +327 -0
  128. package/packages/irc-core/src/commands/multiline.ts +256 -0
  129. package/packages/irc-core/src/commands/names.ts +3 -5
  130. package/packages/irc-core/src/commands/part.ts +2 -4
  131. package/packages/irc-core/src/commands/pre-away.ts +112 -0
  132. package/packages/irc-core/src/commands/privmsg.ts +19 -12
  133. package/packages/irc-core/src/commands/read-marker.ts +108 -0
  134. package/packages/irc-core/src/commands/registration.ts +8 -0
  135. package/packages/irc-core/src/commands/rehash.ts +119 -0
  136. package/packages/irc-core/src/commands/sasl.ts +24 -1
  137. package/packages/irc-core/src/commands/setname.ts +109 -0
  138. package/packages/irc-core/src/commands/stats.ts +152 -0
  139. package/packages/irc-core/src/commands/tagmsg.ts +71 -1
  140. package/packages/irc-core/src/commands/topic.ts +2 -4
  141. package/packages/irc-core/src/commands/trace.ts +137 -0
  142. package/packages/irc-core/src/commands/wallops.ts +118 -0
  143. package/packages/irc-core/src/commands/whois.ts +5 -0
  144. package/packages/irc-core/src/config.ts +72 -10
  145. package/packages/irc-core/src/effects.ts +41 -1
  146. package/packages/irc-core/src/index.ts +2 -0
  147. package/packages/irc-core/src/ports.ts +568 -0
  148. package/packages/irc-core/src/protocol/index.ts +14 -0
  149. package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
  150. package/packages/irc-core/src/protocol/numerics.ts +57 -11
  151. package/packages/irc-core/src/protocol/outbound.ts +36 -4
  152. package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
  153. package/packages/irc-core/src/state/connection.ts +32 -1
  154. package/packages/irc-core/src/types.ts +120 -1
  155. package/packages/irc-core/src/ws-framing.ts +132 -0
  156. package/packages/irc-core/src/ws-subprotocol.ts +66 -0
  157. package/packages/irc-core/stryker.commands.conf.json +1 -2
  158. package/packages/irc-core/tests/admission.test.ts +18 -0
  159. package/packages/irc-core/tests/away-store.test.ts +73 -0
  160. package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
  161. package/packages/irc-core/tests/caps/sts.test.ts +123 -0
  162. package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
  163. package/packages/irc-core/tests/commands/away.test.ts +85 -2
  164. package/packages/irc-core/tests/commands/cap.test.ts +165 -0
  165. package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
  166. package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
  167. package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
  168. package/packages/irc-core/tests/commands/kill.test.ts +243 -0
  169. package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
  170. package/packages/irc-core/tests/commands/mode.test.ts +129 -0
  171. package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
  172. package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
  173. package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
  174. package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
  175. package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
  176. package/packages/irc-core/tests/commands/registration.test.ts +64 -0
  177. package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
  178. package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
  179. package/packages/irc-core/tests/commands/setname.test.ts +225 -0
  180. package/packages/irc-core/tests/commands/stats.test.ts +294 -0
  181. package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
  182. package/packages/irc-core/tests/commands/trace.test.ts +282 -0
  183. package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
  184. package/packages/irc-core/tests/commands/whois.test.ts +34 -0
  185. package/packages/irc-core/tests/config.test.ts +103 -13
  186. package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
  187. package/packages/irc-core/tests/effects.test.ts +14 -0
  188. package/packages/irc-core/tests/labeled-response.test.ts +181 -0
  189. package/packages/irc-core/tests/message-tags.test.ts +114 -0
  190. package/packages/irc-core/tests/monitor-store.test.ts +200 -0
  191. package/packages/irc-core/tests/numerics.test.ts +90 -0
  192. package/packages/irc-core/tests/outbound.test.ts +51 -0
  193. package/packages/irc-core/tests/ports.test.ts +22 -0
  194. package/packages/irc-core/tests/raw-modules.d.ts +11 -0
  195. package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
  196. package/packages/irc-core/tests/standard-replies.test.ts +412 -0
  197. package/packages/irc-core/tests/stats-store.test.ts +222 -0
  198. package/packages/irc-core/tests/types.test.ts +27 -0
  199. package/packages/irc-core/tests/ws-framing.test.ts +213 -0
  200. package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
  201. package/packages/irc-server/package.json +1 -1
  202. package/packages/irc-server/src/actor.ts +699 -19
  203. package/packages/irc-server/src/dispatch.ts +109 -16
  204. package/packages/irc-server/src/routing.ts +3 -0
  205. package/packages/irc-server/src/runtime.ts +31 -0
  206. package/packages/irc-server/src/transport.ts +10 -7
  207. package/packages/irc-server/tests/actor.test.ts +2523 -42
  208. package/packages/irc-server/tests/dispatch.test.ts +300 -2
  209. package/packages/irc-server/tests/raw-modules.d.ts +11 -0
  210. package/packages/irc-server/tests/routing.test.ts +1 -0
  211. package/packages/irc-server/tests/runtime.test.ts +7 -0
  212. package/packages/irc-test-support/package.json +1 -1
  213. package/packages/irc-test-support/src/index.ts +6 -0
  214. package/packages/irc-test-support/src/scenarios.ts +9 -1
  215. package/packages/irc-test-support/src/test-config.ts +54 -0
  216. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
  217. package/packages/irc-test-support/tests/test-config.test.ts +51 -0
  218. package/pnpm-workspace.yaml +1 -0
  219. package/tools/ci-hardening/package.json +1 -1
  220. package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
  221. package/tools/load-test/package.json +33 -0
  222. package/tools/load-test/src/client.ts +351 -0
  223. package/tools/load-test/src/config.ts +313 -0
  224. package/tools/load-test/src/harness.ts +116 -0
  225. package/tools/load-test/src/main.ts +120 -0
  226. package/tools/load-test/src/metrics.ts +168 -0
  227. package/tools/load-test/src/report.ts +106 -0
  228. package/tools/load-test/tests/client.test.ts +212 -0
  229. package/tools/load-test/tests/config.test.ts +152 -0
  230. package/tools/load-test/tests/framing.test.ts +37 -0
  231. package/tools/load-test/tests/harness.test.ts +165 -0
  232. package/tools/load-test/tests/metrics.test.ts +174 -0
  233. package/tools/load-test/tests/report.test.ts +161 -0
  234. package/tools/load-test/tests/smoke.test.ts +67 -0
  235. package/tools/load-test/tsconfig.build.json +12 -0
  236. package/tools/load-test/tsconfig.test.json +10 -0
  237. package/tools/load-test/vitest.config.ts +29 -0
  238. package/tools/package.json +6 -1
  239. package/tools/seed-cf-accounts.ts +4 -1
  240. package/tools/tcp-ws-forwarder/package.json +1 -1
  241. package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
  242. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
  243. package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
@@ -17,25 +17,72 @@
17
17
  * pipeline can be reconstructed end-to-end.
18
18
  */
19
19
 
20
- import type { BroadcastEffect, Effect, Logger, RawLine } from '@serverless-ircd/irc-core';
21
- import { LogLevel } from '@serverless-ircd/irc-core';
20
+ import type {
21
+ BroadcastEffect,
22
+ Effect,
23
+ Logger,
24
+ MonitorStore,
25
+ RawLine,
26
+ } from '@serverless-ircd/irc-core';
27
+ import { LogLevel, filterClientTags } from '@serverless-ircd/irc-core';
22
28
  import type { IrcRuntime } from './runtime.js';
23
29
 
24
- /** Discriminated-union handler: each Effect tag maps to one runtime call. */
25
- type Handler<E extends Effect> = (effect: E, runtime: IrcRuntime) => Promise<void>;
30
+ /**
31
+ * Discriminated-union handler: each Effect tag maps to one runtime call.
32
+ * The handler also receives the bound {@link MonitorStore} (or
33
+ * `undefined`) so ReserveNick / ChangeNick / ReleaseNick can react to the
34
+ * runtime's success/failure and enqueue `730`/`731` pushes for subscribed
35
+ * watchers.
36
+ */
37
+ type Handler<E extends Effect> = (
38
+ effect: E,
39
+ runtime: IrcRuntime,
40
+ monitor: MonitorStore | undefined,
41
+ ) => Promise<void>;
26
42
 
27
43
  const handlers: { [K in Effect['tag']]: Handler<Extract<Effect, { tag: K }>> } = {
28
44
  Send: (e, r) => r.send(e.to, e.lines),
29
45
  Broadcast: (e, r) => broadcast(e, r),
30
46
  Disconnect: (e, r) => r.disconnect(e.conn, e.reason),
31
47
  SendToNick: (e, r) => r.sendToNick(e.nick, e.sender, e.lines, e.notFoundLines),
32
- ReserveNick: (e, r) => r.reserveNick(e.nick, e.conn).then(noop),
33
- ChangeNick: (e, r) => r.changeNick(e.conn, e.oldNick, e.newNick).then(noop),
34
- ReleaseNick: (e, r) => r.releaseNick(e.nick),
48
+ ReserveNick: (e, r, m) => reserveNickEtc(e, r, m),
49
+ ChangeNick: (e, r, m) => changeNickEtc(e, r, m),
50
+ ReleaseNick: (e, r, m) => releaseNickEtc(e, r, m),
35
51
  ApplyChannelDelta: (e, r) => r.applyChannelDelta(e.chan, e.delta),
52
+ BroadcastWallops: (e, r) => r.broadcastWallops(e.lines, e.except),
36
53
  };
37
54
 
38
- function noop(): void {}
55
+ async function reserveNickEtc(
56
+ e: Extract<Effect, { tag: 'ReserveNick' }>,
57
+ r: IrcRuntime,
58
+ m: MonitorStore | undefined,
59
+ ): Promise<void> {
60
+ const result = await r.reserveNick(e.nick, e.conn);
61
+ if (result.ok) {
62
+ m?.onNickOnline(e.nick);
63
+ }
64
+ }
65
+
66
+ async function changeNickEtc(
67
+ e: Extract<Effect, { tag: 'ChangeNick' }>,
68
+ r: IrcRuntime,
69
+ m: MonitorStore | undefined,
70
+ ): Promise<void> {
71
+ const ok = await r.changeNick(e.conn, e.oldNick, e.newNick);
72
+ if (ok) {
73
+ m?.onNickOffline(e.oldNick);
74
+ m?.onNickOnline(e.newNick);
75
+ }
76
+ }
77
+
78
+ async function releaseNickEtc(
79
+ e: Extract<Effect, { tag: 'ReleaseNick' }>,
80
+ r: IrcRuntime,
81
+ m: MonitorStore | undefined,
82
+ ): Promise<void> {
83
+ await r.releaseNick(e.nick);
84
+ m?.onNickOffline(e.nick);
85
+ }
39
86
 
40
87
  /**
41
88
  * Optional knobs for {@link dispatch}. All observability-related; the
@@ -58,6 +105,16 @@ export interface DispatchOptions {
58
105
  * Stamped on every record alongside `traceId`.
59
106
  */
60
107
  readonly connectionId?: string;
108
+ /**
109
+ * Optional {@link MonitorStore} consulted by the ReserveNick /
110
+ * ChangeNick / ReleaseNick handlers. When supplied, successful nick
111
+ * registry transitions fan out an asynchronous `730`/`731` push to
112
+ * every subscribed watcher of the affected nick. The actor drains
113
+ * those pushes after `dispatch` returns and emits one `Send` effect
114
+ * per push. Omitted (→ no monitor fanout) preserves the existing
115
+ * dispatch behaviour for callers that have not opted into MONITOR.
116
+ */
117
+ readonly monitor?: MonitorStore;
61
118
  }
62
119
 
63
120
  /**
@@ -106,17 +163,22 @@ export async function dispatch(
106
163
  previous = effect;
107
164
  continue;
108
165
  }
109
- await handle(effect, runtime, opts);
166
+ await handle(effect, runtime, opts, opts?.monitor);
110
167
  previous = effect;
111
168
  }
112
169
  }
113
170
 
114
- async function handle(effect: Effect, runtime: IrcRuntime, opts?: DispatchOptions): Promise<void> {
171
+ async function handle(
172
+ effect: Effect,
173
+ runtime: IrcRuntime,
174
+ opts: DispatchOptions | undefined,
175
+ monitor: MonitorStore | undefined,
176
+ ): Promise<void> {
115
177
  // The dispatch table is keyed by tag; the cast is the standard TS pattern
116
178
  // for exhaustive-union dispatch.
117
179
  const handler = handlers[effect.tag] as Handler<Effect>;
118
180
  try {
119
- await handler(effect, runtime);
181
+ await handler(effect, runtime, monitor);
120
182
  } catch (err) {
121
183
  if (opts?.logger !== undefined) {
122
184
  opts.logger.on(logLevelFor(err), 'dispatch.error', {
@@ -174,11 +236,18 @@ function logLevelFor(_err: unknown): LogLevel {
174
236
  * - `cap` without `capLines` (cap-only mode): `lines` go ONLY to
175
237
  * cap-enabled members; the rest receive nothing.
176
238
  *
239
+ * `caps` (plural) is the OR-semantics variant: `lines` go to members that
240
+ * announced ANY of the listed caps. Used by capabilities like
241
+ * `draft/typing` whose delivery rule unions `message-tags` (the baseline
242
+ * for every TAGMSG) with `draft/typing` (the client-tag exception for
243
+ * peers that did not negotiate `message-tags`). `caps` does not support
244
+ * a `capLines` split — both audiences receive the same `lines`.
245
+ *
177
246
  * `except` is always honored. A member whose connection info is unavailable
178
247
  * (the connection has gone) is treated as lacking the cap.
179
248
  */
180
249
  async function broadcast(e: BroadcastEffect, r: IrcRuntime): Promise<void> {
181
- if (e.cap === undefined) {
250
+ if (e.cap === undefined && e.caps === undefined) {
182
251
  await r.broadcast(e.chan, e.lines, e.except);
183
252
  return;
184
253
  }
@@ -186,16 +255,40 @@ async function broadcast(e: BroadcastEffect, r: IrcRuntime): Promise<void> {
186
255
  const snapshot = await r.getChannelSnapshot(e.chan);
187
256
  if (snapshot === null) return;
188
257
 
258
+ // `caps` (OR semantics) takes precedence; otherwise the single `cap` is
259
+ // the only entry. The early-return above guarantees one of them is set.
260
+ const capsList: readonly string[] = e.caps ?? [e.cap as string];
189
261
  for (const connId of snapshot.members.keys()) {
190
262
  if (connId === e.except) continue;
191
263
 
192
264
  const info = await r.getConnectionInfo(connId);
193
- const hasCap = info?.caps.has(e.cap) ?? false;
265
+ const recipientCaps = info?.caps ?? EMPTY_CAPS;
266
+ const hasAnyCap = capsList.some((c) => recipientCaps.has(c));
194
267
 
268
+ let toSend: RawLine[] | undefined;
195
269
  if (e.capLines !== undefined) {
196
- await r.send(connId, hasCap ? e.capLines : e.lines);
197
- } else if (hasCap) {
198
- await r.send(connId, e.lines);
270
+ toSend = hasAnyCap ? e.capLines : e.lines;
271
+ } else if (hasAnyCap) {
272
+ toSend = e.lines;
273
+ }
274
+ if (toSend !== undefined) {
275
+ await r.send(connId, filterLinesForRecipient(toSend, recipientCaps));
199
276
  }
200
277
  }
201
278
  }
279
+
280
+ /**
281
+ * Applies per-recipient IRCv3 message-tags gating to each outbound line.
282
+ * Client-only tags (`+`-prefixed keys) are stripped for recipients that did
283
+ * NOT negotiate `message-tags` (with the documented `draft/typing` /
284
+ * `draft/read-marker` whitelist exceptions). Recipients that DID negotiate
285
+ * `message-tags` see the line verbatim. Lines with no `@` tag section pass
286
+ * through unchanged.
287
+ */
288
+ function filterLinesForRecipient(lines: readonly RawLine[], caps: ReadonlySet<string>): RawLine[] {
289
+ if (caps.has('message-tags')) return [...lines];
290
+ return lines.map((l) => filterClientTags(l, caps));
291
+ }
292
+
293
+ /** Reusable empty cap set for recipients whose connection info is unavailable. */
294
+ const EMPTY_CAPS: ReadonlySet<string> = new Set<string>();
@@ -52,6 +52,7 @@ import {
52
52
  privmsgChannelReducer,
53
53
  privmsgUserReducer,
54
54
  quitReducer,
55
+ setnameReducer,
55
56
  tagmsgChannelReducer,
56
57
  tagmsgUserReducer,
57
58
  timeReducer,
@@ -80,6 +81,7 @@ export interface RoutedReducers {
80
81
  readonly cap: Reducer<ConnectionState>;
81
82
  readonly authenticate: Reducer<ConnectionState>;
82
83
  readonly away: Reducer<ConnectionState>;
84
+ readonly setname: Reducer<ConnectionState>;
83
85
  readonly version: Reducer<ConnectionState>;
84
86
  readonly time: Reducer<ConnectionState>;
85
87
  readonly admin: Reducer<ConnectionState>;
@@ -150,6 +152,7 @@ export function buildRoutedReducers(serverConfig: ServerConfig): RoutedReducers
150
152
  cap: wrap(capReducer),
151
153
  authenticate: wrap(authenticateReducer),
152
154
  away: wrap(awayReducer),
155
+ setname: wrap(setnameReducer),
153
156
  version: wrap(versionReducer),
154
157
  time: wrap(timeReducer),
155
158
  admin: wrap(adminReducer),
@@ -17,6 +17,7 @@ import type {
17
17
  ConnectionState,
18
18
  Nick,
19
19
  RawLine,
20
+ ServerConfig,
20
21
  } from '@serverless-ircd/irc-core';
21
22
 
22
23
  export interface IrcRuntime {
@@ -35,6 +36,24 @@ export interface IrcRuntime {
35
36
  lines: RawLine[],
36
37
  notFoundLines?: RawLine[],
37
38
  ): Promise<void>;
39
+ /**
40
+ * Global cross-connection fanout for `WALLOPS`. Delivers `lines` to every
41
+ * live connection whose `+w` user mode is set, skipping `except` (the
42
+ * originator, so an oper never receives their own wallops). Unlike
43
+ * {@link broadcast} this is NOT channel-scoped — it reaches connections
44
+ * with no shared channel membership.
45
+ *
46
+ * Each adapter applies its own cost cap and enumeration strategy:
47
+ * - in-memory: scans its connection map, filters `+w`.
48
+ * - Cloudflare: walks the sharded registry to enumerate connIds, then
49
+ * fetches each state to check `+w`. Capped to bound the fan-out RPC
50
+ * count; document a ceiling for very large fleets.
51
+ * - AWS: `Scan`s the `Connections` table and `PostToConnection`s each
52
+ * `+w` row. DynamoDB `Scan` reads scale with table size, so this is
53
+ * expensive at high connection counts — callers should bound the
54
+ * oper's WALLOPS rate.
55
+ */
56
+ broadcastWallops(lines: RawLine[], except?: ConnId): Promise<void>;
38
57
 
39
58
  // nickname registry
40
59
  reserveNick(nick: Nick, conn: ConnId): Promise<{ ok: true } | { ok: false }>;
@@ -58,4 +77,16 @@ export interface IrcRuntime {
58
77
  getChannelSnapshot(chan: ChanName): Promise<ChanSnapshot | null>;
59
78
  getChannelConnections(chan: ChanName): Promise<ReadonlyMap<ConnId, ConnectionState>>;
60
79
  listChannels(): Promise<ChanSnapshot[]>;
80
+ /**
81
+ * Re-fetches the deployment's {@link ServerConfig} from the adapter's bound
82
+ * source (CF: KV/secret env; AWS: Secrets Manager/SSM env; local-cli: the
83
+ * config file/options) and returns the fresh value. The caller (the
84
+ * ConnectionActor's `REHASH` route) swaps its live config reference with
85
+ * the result; on rejection the previous config stays in effect.
86
+ *
87
+ * Modelled as a request/response runtime method — like `lookupNick` /
88
+ * `getConnection` — rather than a fire-and-forget `Effect`, because the
89
+ * refreshed value must come back to the actor.
90
+ */
91
+ reloadConfig(): Promise<ServerConfig>;
61
92
  }
@@ -15,6 +15,10 @@
15
15
  * parser/reducer/dispatch pipeline.
16
16
  */
17
17
 
18
+ import { splitFrameLines } from '@serverless-ircd/irc-core';
19
+
20
+ export { splitFrameLines };
21
+
18
22
  /**
19
23
  * The line-framing contract a {@link ConnectionActor} consumes. `feed`
20
24
  * returns the complete IRC lines terminated within `chunk`; a stateful
@@ -25,14 +29,13 @@ export interface Transport {
25
29
  }
26
30
 
27
31
  /**
28
- * Splits a WebSocket text frame into IRC lines. Tolerates `\r\n`, bare `\n`,
29
- * and a missing trailing terminator (a WS frame is a complete unit). Returns
30
- * each line bare (no CRLF). This is the legacy-tolerant WS contract.
32
+ * Splits a WebSocket text frame into IRC lines. Re-exported from
33
+ * `@serverless-ircd/irc-core` (`ws-framing.ts`), which owns the canonical
34
+ * legacy-tolerant framing so the IRCv3 spec-mode framing and this legacy
35
+ * behaviour never diverge. Tolerates `\r\n`, bare `\n`, and a missing
36
+ * trailing terminator (a WS frame is a complete unit). Returns each line
37
+ * bare (no CRLF). This is the legacy-tolerant WS contract.
31
38
  */
32
- export function splitFrameLines(text: string): string[] {
33
- if (text.length === 0) return [];
34
- return text.split(/\r?\n/u);
35
- }
36
39
 
37
40
  /**
38
41
  * Stateless WS-frame transport: one {@link feed} call per inbound text frame.