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
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Pure reducer for the IRC `WALLOPS` command (oper-only global broadcast).
3
+ *
4
+ * PLAN §2.1 location-of-authority: WALLOPS runs in the Connection entity.
5
+ * The reducer is pure — it enforces the oper gate, validates the message,
6
+ * and emits a single {@link BroadcastWallopsEffect}. The bound runtime's
7
+ * `broadcastWallops` method owns the cross-connection enumeration that
8
+ * the gate cannot observe purely (it does not see which other
9
+ * connections carry the `+w` user mode).
10
+ *
11
+ * Wire format (RFC 2812 §4.7):
12
+ * - `WALLOPS :<message>`
13
+ * - `481 ERR_NOPRIVILEGES`:
14
+ * `:<server> 481 <nick> :Permission Denied - You're not an IRC operator`
15
+ * - `412 ERR_NOTEXTTOSEND`:
16
+ * `:<server> 412 <nick> :No text to send`
17
+ *
18
+ * Behaviour:
19
+ * - Non-oper sender → `481` (checked before the message is inspected, so
20
+ * a non-oper cannot probe the command shape).
21
+ * - Empty / missing message → `412 ERR_NOTEXTTOSEND`. We pick `412` over
22
+ * `461 ERR_NEEDMOREPARAMS` to match the message-bearing commands
23
+ * PRIVMSG/NOTICE, which use `412` for "no text to send". WALLOPS is a
24
+ * single-param message command, so the semantically closer numeric wins.
25
+ * - Otherwise emit `BroadcastWallops([:<oper> WALLOPS :<message>], sender)`
26
+ * so the runtime fans the line out to every `+w` connection.
27
+ *
28
+ * Delivery / skip-self: the originator is ALWAYS passed as `except`. This
29
+ * matches NOTICE/PRIVMSG channel-broadcast semantics (the sender never
30
+ * receives their own message back) and avoids a redundant echo to the
31
+ * oper who just typed it. The `+w` user mode is the *recipient* gate,
32
+ * owned by the runtime — the sender's own `+w` is therefore irrelevant
33
+ * under this default. An oper who wants a delivery receipt can query
34
+ * logs. (RFC leaves delivery to `+w` regardless of oper; we deliberately
35
+ * diverge to match the rest of the codebase's broadcast primitives.)
36
+ *
37
+ * The `+w` user mode is accepted by the MODE reducer (`w → 'wallops'`)
38
+ * and advertised in `004 RPL_MYINFO`, but no command consumed it before
39
+ * this one — setting `+w` used to be a silent no-op. This reducer lights
40
+ * up the consumer.
41
+ */
42
+
43
+ import { Effect } from '../effects.js';
44
+ import type { RawLine } from '../effects.js';
45
+ import { Numerics } from '../protocol/numerics.js';
46
+ import { hostmaskOf } from '../state/connection.js';
47
+ import type { ConnectionState } from '../state/connection.js';
48
+ import type { Ctx, Reducer } from '../types.js';
49
+
50
+ /** Fixed text emitted for `481 ERR_NOPRIVILEGES` (matches `kill.ts` / `mode.ts`). */
51
+ const NOPRIVILEGES_TRAILING_TEXT = "Permission Denied - You're not an IRC operator";
52
+
53
+ /** Fixed text emitted for `412 ERR_NOTEXTTOSEND` (matches `privmsg.ts`). */
54
+ const NOTEXTTOSEND_TRAILING_TEXT = 'No text to send';
55
+
56
+ /**
57
+ * Renders the `:<source> WALLOPS :<message>` wire line that every `+w`
58
+ * connection receives. `<source>` is the oper's full hostmask
59
+ * (`nick!user@host`), consistent with how PRIVMSG/NOTICE/QUIT attribute
60
+ * user-originated messages. When the hostmask is incomplete (e.g. no
61
+ * `user`/`host` yet, which should not happen for a registered oper but is
62
+ * defended against) the bare nick is used, falling back to `*` only when
63
+ * no nick is set at all.
64
+ */
65
+ export function wallopsLine(state: ConnectionState, message: string): RawLine {
66
+ const source = hostmaskOf(state) ?? state.nick ?? '*';
67
+ return { text: `:${source} WALLOPS :${message}` };
68
+ }
69
+
70
+ /**
71
+ * Handles `WALLOPS :<message>`.
72
+ *
73
+ * Enforces the oper gate, stamps `lastSeen`, and on success emits a single
74
+ * {@link BroadcastWallopsEffect} addressed to every `+w` connection. The
75
+ * sender is always excluded (skip-self). The reducer never performs IO.
76
+ */
77
+ export const wallopsReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
78
+ state.lastSeen = ctx.clock.now();
79
+
80
+ if (!state.userModes.oper) {
81
+ return {
82
+ state,
83
+ effects: [
84
+ Effect.send(ctx.connId, [
85
+ numericLine(ctx, Numerics.ERR_NOPRIVILEGES, NOPRIVILEGES_TRAILING_TEXT),
86
+ ]),
87
+ ],
88
+ };
89
+ }
90
+
91
+ const message = msg.params[0];
92
+ if (message === undefined || message.length === 0) {
93
+ return {
94
+ state,
95
+ effects: [
96
+ Effect.send(ctx.connId, [
97
+ numericLine(ctx, Numerics.ERR_NOTEXTTOSEND, NOTEXTTOSEND_TRAILING_TEXT),
98
+ ]),
99
+ ],
100
+ };
101
+ }
102
+
103
+ return {
104
+ state,
105
+ effects: [Effect.broadcastWallops([wallopsLine(state, message)], ctx.connId)],
106
+ };
107
+ };
108
+
109
+ /**
110
+ * Builds a `:<server> <code> <nick> :<trailing>` line. The nick falls back
111
+ * to `*` when the connection has not yet registered one, matching the rest
112
+ * of the codebase's numeric formatting.
113
+ */
114
+ function numericLine(ctx: Ctx, code: number, trailing: string): RawLine {
115
+ const nick = ctx.connection.nick ?? '*';
116
+ const codeStr = code.toString().padStart(3, '0');
117
+ return { text: [`:${ctx.serverName}`, codeStr, nick, `:${trailing}`].join(' ') };
118
+ }
@@ -134,6 +134,11 @@ export function whoisReducer(
134
134
  text: `:${ctx.serverName} ${Numerics.RPL_WHOISOPERATOR.toString().padStart(3, '0')} ${requesterNick} ${tNick} :is an IRC operator`,
135
135
  });
136
136
  }
137
+ if (target.userModes.tls) {
138
+ lines.push({
139
+ text: `:${ctx.serverName} ${Numerics.RPL_WHOISSECURE.toString().padStart(3, '0')} ${requesterNick} ${tNick} :is using a secure connection`,
140
+ });
141
+ }
137
142
  if (target.account !== undefined) {
138
143
  lines.push({
139
144
  text: `:${ctx.serverName} ${Numerics.RPL_WHOISACCOUNT.toString().padStart(3, '0')} ${requesterNick} ${tNick} ${target.account} :is logged in as`,
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { z } from 'zod';
20
20
  import { BUILD_DATE } from './build-info.js';
21
+ import { DEFAULT_MULTILINE_MAX_BYTES } from './caps/capabilities.js';
21
22
 
22
23
  /**
23
24
  * Schema for a single IRC operator credential.
@@ -61,6 +62,33 @@ export const CloakingConfigSchema = z.object({
61
62
  cloakedSuffix: z.string().min(1).optional(),
62
63
  });
63
64
 
65
+ /**
66
+ * IRCv3 `sts` (Strict Transport Security) policy. When supplied, the
67
+ * server advertises an `sts` capability whose value tells compliant
68
+ * clients how long to pin TLS and which port to upgrade to on a
69
+ * plaintext connection. Omit entirely (the default) to disable STS.
70
+ *
71
+ * Spec: https://ircv3.net/specs/extensions/sts
72
+ */
73
+ export const StsConfigSchema = z.object({
74
+ /**
75
+ * Pin duration in seconds. Defaults to 30 days at the cap-builder layer
76
+ * when omitted; supply an explicit value in production.
77
+ */
78
+ duration: z.number().int().positive(),
79
+ /**
80
+ * TLS listener port advertised on plaintext connections. The canonical
81
+ * IRC-over-TLS port is 6697 (RFC 7193). Defaults to 6697 at the
82
+ * cap-builder layer when omitted.
83
+ */
84
+ port: z.number().int().positive(),
85
+ /**
86
+ * Optional preload directive. When true the policy may be hard-coded
87
+ * into a client preload list (only advertised on plaintext connections).
88
+ */
89
+ preload: z.boolean().optional(),
90
+ });
91
+
64
92
  /**
65
93
  * Per-IP connection-rate limit. Caps how many new connections a single
66
94
  * source IP may open within a sliding `windowMs` window before the
@@ -256,6 +284,13 @@ export const ServerConfigSchema = z.object({
256
284
  channelLen: z.number().int().positive().default(50),
257
285
  /** Maximum topic length (advertised as `TOPICLEN=`). */
258
286
  topicLen: z.number().int().nonnegative().default(390),
287
+ /**
288
+ * Maximum realname (GECOS) length enforced by `SETNAME`. There is no
289
+ * standard ISUPPORT token for this; registration-time `USER` does not
290
+ * enforce a cap, so the knob is optional on {@link ServerConfig} and the
291
+ * reducer falls back to a built-in default when unset.
292
+ */
293
+ realnameLen: z.number().int().nonnegative().default(50),
259
294
  /** Default QUIT reason when the client disconnects without one. */
260
295
  quitMessage: z.string().default('Client Quit'),
261
296
 
@@ -265,6 +300,43 @@ export const ServerConfigSchema = z.object({
265
300
  * lower per deployment. Only consulted when a MessageStore is bound.
266
301
  */
267
302
  chatHistoryPlaybackLimit: z.number().int().positive().default(50),
303
+
304
+ /**
305
+ * IRCv3 `sts` (Strict Transport Security) policy. Omit entirely (the
306
+ * default) to disable STS advertisement; supply `{ duration, port }`
307
+ * (plus optional `preload`) to advertise the cap and let compliant
308
+ * clients upgrade from plaintext to TLS and pin the secure listener.
309
+ */
310
+ sts: StsConfigSchema.optional(),
311
+
312
+ /**
313
+ * IRCv3 `draft/multiline` byte budget. Advertised as
314
+ * `draft/multiline=<n>` in `CAP LS` and enforced on every finalized
315
+ * multi-line batch. Defaults to the IRCv3-recommended 4096; raise or
316
+ * lower per deployment.
317
+ */
318
+ multilineMaxBytes: z.number().int().positive().default(DEFAULT_MULTILINE_MAX_BYTES),
319
+
320
+ /**
321
+ * Per-connection MONITOR watchlist ceiling, advertised as `MONITOR=<n>`
322
+ * in `005 RPL_ISUPPORT`. Omit to inherit the monitor reducer's built-in
323
+ * ceiling (DEFAULT_MONITOR_LIMIT, 30).
324
+ */
325
+ monitorLimit: z.number().int().positive().optional(),
326
+
327
+ /**
328
+ * Extended-ban prefix sigil for `EXTBAN=<prefix>,…` in
329
+ * `005 RPL_ISUPPORT`. Omit for the IRC-standard `~`.
330
+ */
331
+ extbanPrefix: z.string().min(1).optional(),
332
+
333
+ /**
334
+ * Enables IRC-services-derived ISUPPORT tokens (currently
335
+ * `ACCOUNTEXTBAN=a`). Set to true once a `ServicesStore` is bound; until
336
+ * then account-name extended bans are suppressed because unregistered
337
+ * nicks make them meaningless.
338
+ */
339
+ servicesEnabled: z.boolean().optional(),
268
340
  });
269
341
 
270
342
  /** Input type: what adapters supply (fields with defaults may be omitted). */
@@ -296,16 +368,6 @@ export function parseServerConfig(input: unknown): ParsedServerConfig {
296
368
  return result.data;
297
369
  }
298
370
 
299
- /**
300
- * Default server config: the result of parsing an empty input. Useful as a
301
- * known-good baseline for tests and for adapters that want to overlay
302
- * per-deployment overrides via object spread.
303
- */
304
- export const DEFAULT_SERVER_CONFIG: ParsedServerConfig = ServerConfigSchema.parse({
305
- serverName: 'irc.example.com',
306
- networkName: 'ServerlessIRCd',
307
- });
308
-
309
371
  /**
310
372
  * Resolves the effective server version for wire output: the configured
311
373
  * value when present, otherwise {@link DEFAULT_SERVER_VERSION}. Used by the
@@ -43,6 +43,18 @@ export interface BroadcastEffect {
43
43
  except?: ConnId;
44
44
  cap?: string;
45
45
  capLines?: RawLine[];
46
+ /**
47
+ * OR-semantics cap list: the recipient receives `lines` iff they
48
+ * negotiated ANY of the listed caps. Used by capabilities whose delivery
49
+ * rule is the union of two caps — e.g. `draft/typing` TAGMSGs reach
50
+ * peers that announced `draft/typing` OR `message-tags` (the latter
51
+ * because every TAGMSG goes to message-tags peers regardless of tag).
52
+ *
53
+ * Mutually exclusive with {@link cap}: set one or the other, not both.
54
+ * When neither is set, dispatch takes the runtime's bulk broadcast fast
55
+ * path.
56
+ */
57
+ caps?: readonly string[];
46
58
  }
47
59
 
48
60
  export interface DisconnectEffect {
@@ -89,6 +101,26 @@ export interface SendToNickEffect {
89
101
  notFoundLines?: RawLine[];
90
102
  }
91
103
 
104
+ /**
105
+ * Global cross-connection fanout for `WALLOPS`. Unlike
106
+ * {@link BroadcastEffect} (channel-scoped via the roster), this reaches
107
+ * every live connection whose `+w` user mode is set, regardless of shared
108
+ * channel membership. `except` skips a single connection — by convention
109
+ * the originator, so an oper never receives their own wallops back
110
+ * (matching NOTICE/PRIVMSG channel-broadcast semantics).
111
+ *
112
+ * The reducer does not know which connections carry `+w`; the bound
113
+ * runtime's `broadcastWallops` method owns that enumeration. On the
114
+ * in-memory runtime that is a scan of its connection map; on CF it walks
115
+ * the registry shards; on AWS it scans the `Connections` table. Each
116
+ * adapter applies its own cost cap (documented at the method).
117
+ */
118
+ export interface BroadcastWallopsEffect {
119
+ tag: 'BroadcastWallops';
120
+ lines: RawLine[];
121
+ except?: ConnId;
122
+ }
123
+
92
124
  /** Discriminated union of every effect the dispatch interpreter knows. */
93
125
  export type Effect =
94
126
  | SendEffect
@@ -98,7 +130,8 @@ export type Effect =
98
130
  | ChangeNickEffect
99
131
  | ReleaseNickEffect
100
132
  | ApplyChannelDeltaEffect
101
- | SendToNickEffect;
133
+ | SendToNickEffect
134
+ | BroadcastWallopsEffect;
102
135
 
103
136
  /** Literal string tag of every {@link Effect} variant. */
104
137
  export type EffectTag = Effect['tag'];
@@ -129,11 +162,13 @@ export const Effect = {
129
162
  except?: ConnId,
130
163
  cap?: string,
131
164
  capLines?: RawLine[],
165
+ caps?: readonly string[],
132
166
  ): Effect {
133
167
  const e: BroadcastEffect = { tag: 'Broadcast', chan, lines };
134
168
  if (except !== undefined) e.except = except;
135
169
  if (cap !== undefined) e.cap = cap;
136
170
  if (capLines !== undefined) e.capLines = capLines;
171
+ if (caps !== undefined) e.caps = caps;
137
172
  return e;
138
173
  },
139
174
  disconnect(conn: ConnId, reason?: string): Effect {
@@ -158,4 +193,9 @@ export const Effect = {
158
193
  if (notFoundLines !== undefined) e.notFoundLines = notFoundLines;
159
194
  return e;
160
195
  },
196
+ broadcastWallops(lines: RawLine[], except?: ConnId): Effect {
197
+ const e: BroadcastWallopsEffect = { tag: 'BroadcastWallops', lines };
198
+ if (except !== undefined) e.except = except;
199
+ return e;
200
+ },
161
201
  } as const;
@@ -14,6 +14,8 @@ export * from './credential-hashing.js';
14
14
  export * from './flood-control.js';
15
15
  export * from './cloak.js';
16
16
  export * from './case-fold.js';
17
+ export * from './ws-subprotocol.js';
18
+ export * from './ws-framing.js';
17
19
  export * from './admission.js';
18
20
  export * from './config.js';
19
21
  export type {