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,133 @@
1
+ /**
2
+ * IRCv3 `labeled-response` — correlate a request with its replies.
3
+ *
4
+ * Spec: https://ircv3.net/specs/extensions/labeled-response
5
+ *
6
+ * A client tags an inbound message with the `+label=<id>` client tag; the
7
+ * server then wraps every reply caused by that message (addressed back to
8
+ * the requester) in a single `BATCH +id labeled-response … BATCH -id`
9
+ * frame. If the request produces no reply for the requester, the server
10
+ * emits an empty batch so the client can still correlate the label with a
11
+ * definitive "no replies" answer.
12
+ *
13
+ * Unlike the generic {@link wrapBatch} helper — which elides empty batches
14
+ * because the IRCv3 `batch` spec forbids them — {@link wrapLabeledResponse}
15
+ * ALWAYS emits the start/end markers. The empty-batch case is the whole
16
+ * point of labeled-response: it is the correlation signal when there is
17
+ * nothing else to send.
18
+ *
19
+ * This module is pure data transformation. The actor layer:
20
+ * 1. extracts the label from the parsed message tags,
21
+ * 2. plumbs it onto {@link Ctx.label} so every reducer's emitted effects
22
+ * are uniformly attributable to the labeled request, and
23
+ * 3. runs {@link applyLabelToEffects} over the reducer's `Effect[]` to
24
+ * gather every `Send`-to-self and wrap them (or emit an empty batch).
25
+ *
26
+ * Only `Send` effects addressed to the originating connection are wrapped.
27
+ * Broadcasts / `SendToNick` / `BroadcastWallops` reach OTHER connections
28
+ * and are left in place; the label correlation applies solely to the
29
+ * requester's own reply stream.
30
+ */
31
+
32
+ import { Effect } from '../effects.js';
33
+ import type { Effect as EffectType, RawLine } from '../effects.js';
34
+ import type { ConnId } from '../state/connection.js';
35
+
36
+ /** Wire name of the `labeled-response` capability. */
37
+ export const LABELED_RESPONSE_CAP_NAME = 'labeled-response';
38
+
39
+ /** Wire name of the `+label` client tag carried on the inbound message. */
40
+ export const LABEL_TAG = '+label';
41
+
42
+ /** Batch type token used for every labeled-response frame. */
43
+ const LABELED_RESPONSE_BATCH_TYPE = 'labeled-response';
44
+
45
+ /**
46
+ * Reads the `+label` client tag from a parsed message's tag map.
47
+ *
48
+ * Returns `undefined` when the tag is absent OR present-but-empty, so callers
49
+ * can treat a missing label and a bare `+label` (no value) identically: both
50
+ * mean "this request is unlabeled".
51
+ */
52
+ export function extractLabel(tags: Readonly<Record<string, string>>): string | undefined {
53
+ const value = tags[LABEL_TAG];
54
+ if (value === undefined || value.length === 0) return undefined;
55
+ return value;
56
+ }
57
+
58
+ /**
59
+ * Wraps `lines` in IRCv3 `BATCH +label labeled-response … BATCH -label`
60
+ * framing. ALWAYS emits the start and end markers — even when `lines` is
61
+ * empty — because the labeled-response spec requires an empty batch when a
62
+ * labeled request produces no reply (it is the correlation signal).
63
+ */
64
+ export function wrapLabeledResponse(label: string, lines: readonly RawLine[]): RawLine[] {
65
+ const start: RawLine = { text: `BATCH +${label} ${LABELED_RESPONSE_BATCH_TYPE}` };
66
+ const end: RawLine = { text: `BATCH -${label}` };
67
+ return [start, ...lines, end];
68
+ }
69
+
70
+ /**
71
+ * Options for {@link applyLabelToEffects}.
72
+ *
73
+ * `label` is `string | undefined` (NOT an optional field) so the helper can
74
+ * be a clean no-op under `exactOptionalPropertyTypes` when the inbound
75
+ * request carried no label.
76
+ */
77
+ export interface LabelApplyOptions {
78
+ /** The label extracted from the inbound `+label` tag, or `undefined`. */
79
+ readonly label: string | undefined;
80
+ /** The originating connection id (replies wrapped are addressed here). */
81
+ readonly connId: ConnId;
82
+ }
83
+
84
+ /**
85
+ * Gathers every `Send` effect addressed to `connId` and replaces them with a
86
+ * single `Send` whose lines are wrapped in one `labeled-response` batch.
87
+ *
88
+ * Behaviour:
89
+ * - When `label` is `undefined` the effect list is returned unchanged
90
+ * (copy) — the request was not labeled.
91
+ * - All `Send`-to-`connId` effects, wherever they appear, are accumulated
92
+ * in original order into one batch.
93
+ * - The wrapped batch is placed at the position of the FIRST collected
94
+ * `Send`-to-self, so the reducer's intended ordering relative to other
95
+ * effects (broadcasts, etc.) is preserved.
96
+ * - When NO `Send`-to-self exists, an empty labeled batch is appended at
97
+ * the end — the requester still observes the label correlation.
98
+ * - Every other effect (Broadcast, SendToNick, Disconnect, …) is left in
99
+ * place and in order; only the requester's own reply stream is wrapped.
100
+ */
101
+ export function applyLabelToEffects(
102
+ effects: readonly EffectType[],
103
+ opts: LabelApplyOptions,
104
+ ): EffectType[] {
105
+ const { label, connId } = opts;
106
+ if (label === undefined) return [...effects];
107
+
108
+ const selfLines: RawLine[] = [];
109
+ let firstSelfIdx = -1;
110
+ for (const [i, effect] of effects.entries()) {
111
+ if (effect.tag === 'Send' && effect.to === connId) {
112
+ if (firstSelfIdx === -1) firstSelfIdx = i;
113
+ selfLines.push(...effect.lines);
114
+ }
115
+ }
116
+
117
+ const batchEffect: EffectType = Effect.send(connId, wrapLabeledResponse(label, selfLines));
118
+
119
+ // No self-send to anchor on: append the empty labeled batch at the end.
120
+ if (firstSelfIdx === -1) {
121
+ return [...effects, batchEffect];
122
+ }
123
+
124
+ const out: EffectType[] = [];
125
+ for (const [i, effect] of effects.entries()) {
126
+ // Emit the single wrapped batch where the first self-send lived.
127
+ if (i === firstSelfIdx) out.push(batchEffect);
128
+ // Drop every original self-send (they are now inside the batch).
129
+ if (effect.tag === 'Send' && effect.to === connId) continue;
130
+ out.push(effect);
131
+ }
132
+ return out;
133
+ }
@@ -16,6 +16,36 @@ export const Numerics = {
16
16
  // Capability / stats admin / version
17
17
  RPL_ADMINME: 256,
18
18
 
19
+ // LUSERS (RFC 2812 §4.6.2). Network-wide user/channel/server counts.
20
+ // 251 is the human-readable summary; 252–254 are emitted only when their
21
+ // count is non-zero (per RFC). 255 is the local-server summary. 265/266
22
+ // carry the optional local/global max-connection context.
23
+ RPL_LUSERCLIENT: 251,
24
+ RPL_LUSEROP: 252,
25
+ RPL_LUSERUNKNOWN: 253,
26
+ RPL_LUSERCHANNELS: 254,
27
+ RPL_LUSERME: 255,
28
+ RPL_LOCALUSERS: 265,
29
+ RPL_GLOBALUSERS: 266,
30
+
31
+ // STATS (RFC 2812 §4.6.3 / §5.1). 211 is link-info (the `l` query),
32
+ // 242 is the uptime line (the `u` query), and 219 terminates every
33
+ // STATS reply regardless of query letter. The remaining RPL_STATS*
34
+ // codes (212–218, 240–248) are deferred behind per-letter tests.
35
+ RPL_STATSLINKINFO: 211,
36
+ RPL_ENDOFSTATS: 219,
37
+ RPL_STATSUPTIME: 242,
38
+
39
+ // TRACE (RFC 2812 §4.6.3). In a single-server deployment TRACE collapses
40
+ // to a local-server line (200/206) plus optional per-connection detail
41
+ // (204 oper / 205 user), terminated by 262. Multi-hop S2S tracing is a
42
+ // PLAN non-goal.
43
+ RPL_TRACELINK: 200,
44
+ RPL_TRACEOPERATOR: 204,
45
+ RPL_TRACEUSER: 205,
46
+ RPL_TRACESERVER: 206,
47
+ RPL_ENDOFTRACE: 262,
48
+
19
49
  // Away / userhost / ison
20
50
  RPL_AWAY: 301,
21
51
  RPL_USERHOST: 302,
@@ -23,6 +53,15 @@ export const Numerics = {
23
53
  RPL_UNAWAY: 305,
24
54
  RPL_NOWAWAY: 306,
25
55
 
56
+ // IRCv3 MONITOR (modern ISON replacement). The watchlist lives on
57
+ // {@link ConnectionState.monitorList}; transitions push 730/731
58
+ // asynchronously via the actor's nick-registry hooks.
59
+ RPL_MONONLINE: 730,
60
+ RPL_MONOFFLINE: 731,
61
+ RPL_MONLIST: 732,
62
+ RPL_ENDOFMONLIST: 733,
63
+ ERR_MONLISTFULL: 734,
64
+
26
65
  // Version
27
66
  RPL_VERSION: 351,
28
67
 
@@ -34,6 +73,12 @@ export const Numerics = {
34
73
  RPL_ENDOFWHOIS: 318,
35
74
  RPL_WHOISCHANNELS: 319,
36
75
  RPL_WHOISACCOUNT: 330,
76
+ /**
77
+ * `276 RPL_WHOISSECURE` — target is connected over TLS (user mode `S`).
78
+ * Surfaced between the oper/account lines and the idle line so clients
79
+ * render it as part of the secure-connection WHOIS block.
80
+ */
81
+ RPL_WHOISSECURE: 276,
37
82
  RPL_WHOWASUSER: 314,
38
83
 
39
84
  // LIST
@@ -61,10 +106,8 @@ export const Numerics = {
61
106
  RPL_NAMREPLY: 353,
62
107
  RPL_ENDOFNAMES: 366,
63
108
 
64
- // Links / ban / whowas. RPL_LINKS/RPL_ENDOFLINKS retained for the deferred
65
- // LINKS verb (S2S server list PLAN non-goal; see actor default-route triage).
66
- RPL_LINKS: 364,
67
- RPL_ENDOFLINKS: 365,
109
+ // Ban / whowas. (The S2S `LINKS` verb and its `RPL_LINKS`/`RPL_ENDOFLINKS`
110
+ // numerics were dropped — S2S is a PLAN non-goal.)
68
111
  RPL_BANLIST: 367,
69
112
  RPL_ENDOFBANLIST: 368,
70
113
  RPL_ENDOFWHOWAS: 369,
@@ -76,11 +119,12 @@ export const Numerics = {
76
119
  RPL_MOTDSTART: 375,
77
120
  RPL_ENDOFMOTD: 376,
78
121
 
79
- // Oper. RPL_REHASHING retained for the deferred REHASH verb (no runtime
80
- // config-reload path in serverless; see actor default-route triage).
122
+ // Oper. RPL_REHASHING is emitted by the oper-gated `REHASH` reducer when a
123
+ // config reload is requested (the actor re-invokes the adapter's loader).
124
+ // (The RFC 2812 `SERVICE` verb and its `RPL_YOURESERVICE` numeric were
125
+ // dropped — obsolete, never widely implemented.)
81
126
  RPL_YOUREOPER: 381,
82
127
  RPL_REHASHING: 382,
83
- RPL_YOURESERVICE: 383,
84
128
  RPL_TIME: 391,
85
129
 
86
130
  // SASL (IRCv3 sasl-3.2)
@@ -102,7 +146,8 @@ export const Numerics = {
102
146
  ERR_TOOMANYCHANNELS: 405,
103
147
  ERR_WASNOSUCHNICK: 406,
104
148
  ERR_TOOMANYTARGETS: 407,
105
- ERR_NOSUCHSERVICE: 408, // retained: deferred SERVICE verb (RFC 2812 obsolete)
149
+ // `ERR_NOSUCHSERVICE` (408) was dropped with the obsolete RFC 2812 `SERVICE`
150
+ // verb.
106
151
  ERR_INVALIDCAPCMD: 410,
107
152
  ERR_NORECIPIENT: 411,
108
153
  ERR_NOTEXTTOSEND: 412,
@@ -121,8 +166,8 @@ export const Numerics = {
121
166
  ERR_NOTONCHANNEL: 442,
122
167
  ERR_USERONCHANNEL: 443,
123
168
  ERR_NOLOGIN: 444,
124
- ERR_SUMMONDISABLED: 445, // retained: deferred SUMMON verb (RFC 2812 obsolete)
125
- ERR_USERSDISABLED: 446, // retained: deferred USERS verb (RFC 2812 obsolete)
169
+ // `ERR_SUMMONDISABLED` (445) / `ERR_USERSDISABLED` (446) were dropped with
170
+ // the obsolete RFC 2812 `SUMMON`/`USERS` verbs.
126
171
  ERR_NOTREGISTERED: 451,
127
172
  ERR_NEEDMOREPARAMS: 461,
128
173
  ERR_ALREADYREGISTRED: 462,
@@ -140,7 +185,8 @@ export const Numerics = {
140
185
  ERR_BANLISTFULL: 478,
141
186
  ERR_NOPRIVILEGES: 481,
142
187
  ERR_CHANOPRIVSNEEDED: 482,
143
- ERR_CANTKILLSERVER: 483, // retained: deferred SQUIT verb (S2S, PLAN non-goal)
188
+ // `ERR_CANTKILLSERVER` (483) was dropped with the S2S `SQUIT` verb S2S is
189
+ // a PLAN non-goal.
144
190
  ERR_NOOPERHOST: 491,
145
191
  ERR_UMODEUNKNOWNFLAG: 501,
146
192
  ERR_USERSDONTMATCH: 502,
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { RawLine } from '../effects.js';
19
+ import { MAX_WS_MESSAGE_BYTES, type WsFrameMode } from '../ws-framing.js';
19
20
 
20
21
  /**
21
22
  * RFC 1459 §2.3: maximum IRC line length in bytes, including the trailing
@@ -65,7 +66,7 @@ export function applyServerTime(line: RawLine, caps: ReadonlySet<string>, now: n
65
66
  }
66
67
 
67
68
  /**
68
- * Enforces the IRC 512-byte line limit on `line.text`.
69
+ * Enforces the IRC line-length budget on `line.text`.
69
70
  *
70
71
  * For recipients that negotiated `message-tags`, the tag section (the
71
72
  * leading `@... ` prefix) is part of the wire budget and is preserved
@@ -73,11 +74,22 @@ export function applyServerTime(line: RawLine, caps: ReadonlySet<string>, now: n
73
74
  * For legacy recipients the entire `line.text` is the body and may be
74
75
  * truncated from the right.
75
76
  *
77
+ * The optional `mode` selects which byte budget applies: an IRCv3
78
+ * WebSocket spec mode (`spec-text` / `spec-binary`) caps the message at
79
+ * {@link MAX_WS_MESSAGE_BYTES} (510) directly, because WebSocket messages
80
+ * carry no trailing CR-LF; legacy mode (the default) caps at
81
+ * `MAX_LINE_BYTES - 2` (512 − 2 = 510). Both resolve to 510 today, but the
82
+ * mode-aware call sites need no edits if either constant ever changes.
83
+ *
76
84
  * The returned {@link RawLine} is the original object when no truncation
77
85
  * was needed, otherwise a fresh object with the truncated text.
78
86
  */
79
- export function enforceLineLimit(line: RawLine, caps: ReadonlySet<string>): RawLine {
80
- const budget = MAX_LINE_BYTES - TRAILING_CRLF_LEN;
87
+ export function enforceLineLimit(
88
+ line: RawLine,
89
+ caps: ReadonlySet<string>,
90
+ mode?: WsFrameMode,
91
+ ): RawLine {
92
+ const budget = isSpecMode(mode) ? MAX_WS_MESSAGE_BYTES : MAX_LINE_BYTES - TRAILING_CRLF_LEN;
81
93
  if (line.text.length <= budget) return line;
82
94
 
83
95
  const hasTags = caps.has('message-tags') && line.text.startsWith('@');
@@ -114,6 +126,14 @@ export function enforceLineLimit(line: RawLine, caps: ReadonlySet<string>): RawL
114
126
  * Server-defined tags (no `+` prefix) are left in place — each is gated by
115
127
  * its own capability (server-time, account-tag, …) applied elsewhere.
116
128
  *
129
+ * Spec exception: IRCv3 `draft/typing` carves out a whitelist for the
130
+ * `+draft/typing` client tag. A recipient that announced `draft/typing`
131
+ * (but NOT `message-tags`) still receives `+draft/typing` so typing
132
+ * indicators work without the broader message-tags cap. `draft/read-marker`
133
+ * carves out the same exception for its `+draft/read-marker` tag so a
134
+ * read-marker update reaches the user's other connections. Every other
135
+ * client tag remains stripped for such recipients.
136
+ *
117
137
  * When filtering removes every tag, the leading `@... ` section is dropped
118
138
  * entirely so legacy clients never observe a bare tag prefix.
119
139
  *
@@ -130,10 +150,17 @@ export function filterClientTags(line: RawLine, caps: ReadonlySet<string>): RawL
130
150
  const rest = line.text.slice(spaceIdx + 1);
131
151
  const entries = tagSection.split(';');
132
152
 
153
+ const typingWhitelisted = caps.has('draft/typing');
154
+ const readMarkerWhitelisted = caps.has('draft/read-marker');
133
155
  const hasClientTag = entries.some((entry) => clientTagKey(entry).startsWith('+'));
134
156
  if (!hasClientTag) return line;
135
157
 
136
- const kept = entries.filter((entry) => !clientTagKey(entry).startsWith('+'));
158
+ const kept = entries.filter((entry) => {
159
+ const key = clientTagKey(entry);
160
+ if (!key.startsWith('+')) return true;
161
+ if (typingWhitelisted && key === '+draft/typing') return true;
162
+ return readMarkerWhitelisted && key === '+draft/read-marker';
163
+ });
137
164
  if (kept.length === 0) return { text: rest };
138
165
  return { text: `@${kept.join(';')} ${rest}` };
139
166
  }
@@ -143,3 +170,8 @@ function clientTagKey(entry: string): string {
143
170
  const eq = entry.indexOf('=');
144
171
  return eq === -1 ? entry : entry.slice(0, eq);
145
172
  }
173
+
174
+ /** True when `mode` is an IRCv3 WebSocket spec framing mode (not legacy/absent). */
175
+ function isSpecMode(mode: WsFrameMode | undefined): boolean {
176
+ return mode === 'spec-text' || mode === 'spec-binary';
177
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * IRCv3 `standard-replies` — `FAIL` / `WARN` / `NOTE` dispatch helper.
3
+ *
4
+ * Spec: https://ircv3.net/specs/extensions/standard-replies-3.0
5
+ *
6
+ * Modern IRC clients that negotiated the `standard-replies` cap accept the
7
+ * structured reply form
8
+ *
9
+ * :<server> FAIL <command> <code> [<context>...] :<description>
10
+ * :<server> WARN <command> <code> [<context>...] :<description>
11
+ * :<server> NOTE <command> <code> [<context>...] :<description>
12
+ *
13
+ * in place of the legacy `:server NNN nick ... :trailing` numerics for a
14
+ * curated subset of errors. Numeric-only replies (MOTD lines, NAMES,
15
+ * welcome block, …) stay on the numeric form — only the curated error
16
+ * numerics are translated.
17
+ *
18
+ * This module is the data-driven mapping plus a single pure dispatch
19
+ * helper ({@link toStandardReply}) that the dispatch / outbound layer
20
+ * applies per recipient. Translating at the outbound layer (rather than
21
+ * scattering `if (caps.has('standard-replies'))` through every reducer)
22
+ * keeps the reducer code cap-agnostic, matching the model already used
23
+ * for `server-time` and `message-tags`.
24
+ *
25
+ * Reducers keep emitting the canonical numeric line; the dispatch layer
26
+ * runs this helper to translate the curated numerics for cap-enabled
27
+ * recipients and passes everything else through verbatim.
28
+ */
29
+
30
+ import type { RawLine } from '../effects.js';
31
+ import { Numerics } from './numerics.js';
32
+
33
+ /** One of the IRCv3 standard-reply verbs. */
34
+ export type ReplyKind = 'FAIL' | 'WARN' | 'NOTE';
35
+
36
+ /**
37
+ * One row of the {@link numericToStandardReply} table, as a discriminated
38
+ * union over where the COMMAND token comes from on the wire.
39
+ *
40
+ * - When {@link CommandFromMiddle.commandFromMiddle} is `true`, the
41
+ * numeric's middle parameter IS the failed command name (true for
42
+ * `461 ERR_NEEDMOREPARAMS`, whose middle is the command the client
43
+ * undersupplied).
44
+ * - When {@link CommandFixed.commandFromMiddle} is `false`, a fixed
45
+ * {@link CommandFixed.command} is emitted (true for `432`/`433`,
46
+ * always `NICK`, and for SASL failures, always `AUTHENTICATE`).
47
+ *
48
+ * The discriminated union lets `pickCommand` return `mapping.command`
49
+ * directly in the fixed branch with no defensive `?? null` — the type
50
+ * system proves `command` is always set there.
51
+ */
52
+ export type StandardReplyMapping = CommandFromMiddle | CommandFixed;
53
+
54
+ /** A row whose COMMAND token comes from the numeric's middle parameter. */
55
+ export interface CommandFromMiddle {
56
+ /** Discriminant: true → read COMMAND from `parsed.middle[0]`. */
57
+ commandFromMiddle: true;
58
+ /** Which of `FAIL`/`WARN`/`NOTE` to emit. */
59
+ kind: ReplyKind;
60
+ /** Stable wire code (`INVALID_PARAMS`, …). */
61
+ code: string;
62
+ /** Always false here — the middle is consumed as the command, not context. */
63
+ contextFromMiddle: false;
64
+ }
65
+
66
+ /** A row whose COMMAND token is a fixed string. */
67
+ export interface CommandFixed {
68
+ /** Discriminant: false → read COMMAND from {@link command}. */
69
+ commandFromMiddle: false;
70
+ /** Which of `FAIL`/`WARN`/`NOTE` to emit. */
71
+ kind: ReplyKind;
72
+ /** Stable wire code (`NO_SUCH_CHANNEL`, `INVALID_NICK`, …). */
73
+ code: string;
74
+ /** Fixed command token emitted after the FAIL/WARN/NOTE verb. */
75
+ command: string;
76
+ /**
77
+ * True iff the numeric's middle parameter carries a context token
78
+ * (the bad nick, the bad channel) that should follow the code.
79
+ */
80
+ contextFromMiddle: boolean;
81
+ }
82
+
83
+ /**
84
+ * The curated numeric → standard-reply mapping.
85
+ *
86
+ * Only a deliberately small subset of error numerics is translated —
87
+ * the ones that carry enough context for the structured form to be a
88
+ * strict improvement over the bare numeric. Numeric-only families
89
+ * (MOTD, NAMES, the welcome block, mode replies, …) stay on the
90
+ * numeric form per the spec.
91
+ */
92
+ export const numericToStandardReply: ReadonlyMap<number, StandardReplyMapping> = new Map<
93
+ number,
94
+ StandardReplyMapping
95
+ >([
96
+ [
97
+ Numerics.ERR_NEEDMOREPARAMS,
98
+ {
99
+ kind: 'FAIL',
100
+ code: 'INVALID_PARAMS',
101
+ commandFromMiddle: true,
102
+ contextFromMiddle: false,
103
+ },
104
+ ],
105
+ [
106
+ Numerics.ERR_NOSUCHCHANNEL,
107
+ {
108
+ kind: 'FAIL',
109
+ code: 'NO_SUCH_CHANNEL',
110
+ commandFromMiddle: false,
111
+ command: 'JOIN',
112
+ contextFromMiddle: true,
113
+ },
114
+ ],
115
+ [
116
+ Numerics.ERR_ERRONEUSNICKNAME,
117
+ {
118
+ kind: 'FAIL',
119
+ code: 'INVALID_NICK',
120
+ commandFromMiddle: false,
121
+ command: 'NICK',
122
+ contextFromMiddle: true,
123
+ },
124
+ ],
125
+ [
126
+ Numerics.ERR_NICKNAMEINUSE,
127
+ {
128
+ kind: 'FAIL',
129
+ code: 'NICKNAME_IN_USE',
130
+ commandFromMiddle: false,
131
+ command: 'NICK',
132
+ contextFromMiddle: true,
133
+ },
134
+ ],
135
+ [
136
+ Numerics.ERR_SASLFAIL,
137
+ {
138
+ kind: 'FAIL',
139
+ code: 'SASL_FAILED',
140
+ commandFromMiddle: false,
141
+ command: 'AUTHENTICATE',
142
+ contextFromMiddle: false,
143
+ },
144
+ ],
145
+ ]);
146
+
147
+ /**
148
+ * Translates a single outbound {@link RawLine} for a recipient whose
149
+ * negotiated caps are `caps`.
150
+ *
151
+ * - If the recipient lacks the `standard-replies` cap, the line is
152
+ * returned unchanged (the legacy numeric still ships).
153
+ * - If the line is not a `:server NNN …` numeric, it is returned
154
+ * unchanged (PRIVMSG, BATCH, FAIL/WARN/NOTE already emitted by
155
+ * reducers, etc.).
156
+ * - If the numeric is not in {@link numericToStandardReply}, the line
157
+ * is returned unchanged.
158
+ * - Otherwise the line is rewritten into the structured
159
+ * `FAIL`/`WARN`/`NOTE` form using the table.
160
+ *
161
+ * The input is never mutated; the original object is returned whenever
162
+ * no translation occurred so callers can cheaply detect "no change".
163
+ */
164
+ export function toStandardReply(line: RawLine, caps: ReadonlySet<string>): RawLine {
165
+ if (!caps.has('standard-replies')) return line;
166
+ if (!line.text.startsWith(':')) return line;
167
+
168
+ const parsed = parseNumericLine(line.text);
169
+ if (parsed === null) return line;
170
+
171
+ const mapping = numericToStandardReply.get(parsed.code);
172
+ if (mapping === undefined) return line;
173
+
174
+ const command = pickCommand(parsed, mapping);
175
+ if (command === null) return line;
176
+
177
+ // The structured form requires a `:description` token. A curated numeric
178
+ // missing its trailing parameter (malformed input that never ships from a
179
+ // reducer) is left on the bare numeric so we never emit an empty FAIL.
180
+ if (parsed.trailing === undefined) return line;
181
+
182
+ const context = pickContext(parsed, mapping);
183
+ const trailing = parsed.trailing;
184
+
185
+ const head = `:${parsed.server} ${mapping.kind} ${command} ${mapping.code}`;
186
+ const withContext = context !== null ? `${head} ${context}` : head;
187
+ return { text: `${withContext} :${trailing}` };
188
+ }
189
+
190
+ /** Internal: the parsed shape of a `:server NNN nick [middle] [:trailing]` line. */
191
+ interface ParsedNumeric {
192
+ server: string;
193
+ code: number;
194
+ /** Every token between the nick and the trailing `:`-parameter. May be empty. */
195
+ middle: string[];
196
+ /** The trailing `:`-prefixed parameter, with the leading `:` stripped. */
197
+ trailing: string | undefined;
198
+ }
199
+
200
+ /**
201
+ * Parses a single `:server NNN ...` line into its components.
202
+ *
203
+ * Returns `null` for any shape this server does not emit for numerics:
204
+ * lines without a server prefix, lines whose second token is not a
205
+ * three-digit numeric, and lines with no nick token. The trailing
206
+ * `:`-parameter is optional (a handful of numerics — currently none in
207
+ * the curated set — ship without one).
208
+ */
209
+ function parseNumericLine(text: string): ParsedNumeric | null {
210
+ // Strip the leading `:server ` prefix.
211
+ const firstSpace = text.indexOf(' ');
212
+ if (firstSpace === -1) return null;
213
+ const server = text.slice(1, firstSpace);
214
+
215
+ const rest = text.slice(firstSpace + 1);
216
+ const secondSpace = rest.indexOf(' ');
217
+ if (secondSpace === -1) return null;
218
+ const codeTok = rest.slice(0, secondSpace);
219
+ if (!/^\d{3}$/.test(codeTok)) return null;
220
+ const code = Number.parseInt(codeTok, 10);
221
+
222
+ const afterCode = rest.slice(secondSpace + 1);
223
+
224
+ // The nick token is mandatory between the code and the optional trailing.
225
+ // Empty after-code or an `:`-prefixed after-code (the trailing param
226
+ // immediately following the code) is malformed for a numeric this server
227
+ // emits; bail so the caller passes the line through verbatim.
228
+ if (afterCode.length === 0) return null;
229
+ if (afterCode[0] === ':') return null;
230
+
231
+ // Split off the trailing `:`-parameter if present.
232
+ const trailingIdx = findTrailingColon(afterCode);
233
+ let head = afterCode;
234
+ let trailing: string | undefined;
235
+ if (trailingIdx !== -1) {
236
+ head = afterCode.slice(0, trailingIdx).trimEnd();
237
+ trailing = afterCode.slice(trailingIdx + 1);
238
+ }
239
+
240
+ // head starts with the nick and optional middle params.
241
+ const tokens = head.length === 0 ? [] : head.split(' ');
242
+ if (tokens.length === 0) return null;
243
+ // Drop the nick — it is not used in the structured form (the reply is
244
+ // routed to a specific recipient, not addressed by nick).
245
+ const middle = tokens.slice(1);
246
+
247
+ return { server, code, middle, trailing };
248
+ }
249
+
250
+ /**
251
+ * Returns the index of the ` :` that introduces the trailing parameter,
252
+ * or `-1` when the line has no trailing. The leading space is required
253
+ * so a `:` inside a middle token (e.g. a malformed channel name) is not
254
+ * misread.
255
+ */
256
+ function findTrailingColon(s: string): number {
257
+ for (let i = 0; i < s.length; i++) {
258
+ if (s[i] === ' ' && s[i + 1] === ':') return i + 1;
259
+ }
260
+ return -1;
261
+ }
262
+
263
+ /** Resolves the COMMAND token for the structured reply per the mapping. */
264
+ function pickCommand(parsed: ParsedNumeric, mapping: StandardReplyMapping): string | null {
265
+ if (mapping.commandFromMiddle) {
266
+ const cmd = parsed.middle[0];
267
+ return cmd === undefined ? null : cmd;
268
+ }
269
+ return mapping.command;
270
+ }
271
+
272
+ /** Resolves the optional CONTEXT token for the structured reply per the mapping. */
273
+ function pickContext(parsed: ParsedNumeric, mapping: StandardReplyMapping): string | null {
274
+ if (!mapping.contextFromMiddle) return null;
275
+ const ctx = parsed.middle[0];
276
+ return ctx ?? null;
277
+ }
@@ -20,13 +20,21 @@ export type RegistrationState = 'pre-registration' | 'registering' | 'registered
20
20
 
21
21
  /**
22
22
  * User modes applied to a connection. The IRC user-mode letters are
23
- * `i` (invisible), `o` (oper), `w` (wallops), `s` (server notices).
23
+ * `i` (invisible), `o` (oper), `w` (wallops), `s` (server notices),
24
+ * `S` (TLS connected — read-only, set by the transport at registration).
24
25
  */
25
26
  export interface UserModes {
26
27
  invisible: boolean;
27
28
  oper: boolean;
28
29
  wallops: boolean;
29
30
  serverNotices: boolean;
31
+ /**
32
+ * User mode `S` — the connection arrived over TLS (wss or irc+tls).
33
+ * Read-only: set once by the registration reducer from
34
+ * {@link ConnectionState.secure} and never mutated afterwards. Not
35
+ * settable via `MODE` (the parser rejects `+S`/`-S` with `501`).
36
+ */
37
+ tls: boolean;
30
38
  }
31
39
 
32
40
  /**
@@ -84,6 +92,15 @@ export interface ConnectionState {
84
92
  * `CAP END` arrives (per IRCv3 capability-negotiation spec).
85
93
  */
86
94
  capNegotiating: boolean;
95
+ /**
96
+ * Transport-level fact: true when the connection arrived over TLS
97
+ * (`wss://` WebSocket or `irc+tls://` raw TCP). Set once by the adapter
98
+ * at construction from the platform's transport metadata and read by the
99
+ * registration reducer to surface user mode `S` ({@link UserModes.tls}).
100
+ * Defaults to `false` so existing callers (plain ws test harnesses) keep
101
+ * the pre-TLS-mode behaviour.
102
+ */
103
+ secure: boolean;
87
104
  /** IRCv3 capabilities negotiated via `CAP`. */
88
105
  caps: Set<string>;
89
106
  /** Away reason; absent when not away. */
@@ -109,6 +126,18 @@ export interface ConnectionState {
109
126
  * PART/re-JOIN cycle with no new activity replays nothing.
110
127
  */
111
128
  lastReadMarkers?: Map<string, string>;
129
+ /**
130
+ * Per-connection MONITOR watchlist (IRCv3 `MONITOR`). Each entry is the
131
+ * case-folded (rfc1459) spelling of a watched nick; the display spelling
132
+ * is resolved at emission time via the nick registry. Lazily allocated
133
+ * on first `MONITOR +`; persisted verbatim so a reconnect can restore
134
+ * the watchlist without re-issuing every `MONITOR` line.
135
+ *
136
+ * Capped at the deployment's `MONITOR=<n>` ISUPPORT value (default 30);
137
+ * the {@link monitorReducer} enforces the cap and emits `734
138
+ * ERR_MONLISTFULL` for rejected nicks.
139
+ */
140
+ monitorList?: Set<string>;
112
141
  }
113
142
 
114
143
  /**
@@ -147,6 +176,7 @@ export function createConnection(opts: CreateConnectionOptions): ConnectionState
147
176
  id: opts.id,
148
177
  registration: 'pre-registration',
149
178
  capNegotiating: false,
179
+ secure: false,
150
180
  caps: new Set<string>(),
151
181
  joinedChannels: new Set<ChanName>(),
152
182
  userModes: {
@@ -154,6 +184,7 @@ export function createConnection(opts: CreateConnectionOptions): ConnectionState
154
184
  oper: false,
155
185
  wallops: false,
156
186
  serverNotices: false,
187
+ tls: false,
157
188
  },
158
189
  lastSeen: opts.lastSeen ?? opts.connectedSince,
159
190
  connectedSince: opts.connectedSince,