serverless-ircd 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/.github/workflows/ci.yml +34 -2
  2. package/.github/workflows/deploy-aws.yml +1 -2
  3. package/.github/workflows/deploy-cf-tcp.yml +1 -5
  4. package/.github/workflows/deploy-cf.yml +1 -2
  5. package/CHANGELOG.md +260 -0
  6. package/README.md +132 -22
  7. package/apps/aws-stack/README.md +33 -0
  8. package/apps/aws-stack/package.json +1 -1
  9. package/apps/aws-stack/src/aws-stack.ts +74 -9
  10. package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
  11. package/apps/aws-stack/tests/stack.test.ts +2 -2
  12. package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
  13. package/apps/aws-stack/tests/vitest.setup.ts +18 -0
  14. package/apps/aws-stack/vitest.config.ts +3 -0
  15. package/apps/cf-tcp-container/package.json +1 -1
  16. package/apps/cf-tcp-container/src/config-loader.ts +6 -7
  17. package/apps/cf-tcp-container/src/container-server.ts +3 -0
  18. package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
  19. package/apps/cf-tcp-container/wrangler.toml +1 -1
  20. package/apps/cf-worker/package.json +2 -1
  21. package/apps/cf-worker/vitest.config.ts +1 -1
  22. package/apps/cf-worker/wrangler.toml +5 -3
  23. package/apps/local-cli/package.json +1 -1
  24. package/apps/local-cli/src/config-loader.ts +6 -6
  25. package/apps/local-cli/src/main.ts +20 -0
  26. package/apps/local-cli/src/server.ts +9 -7
  27. package/apps/local-cli/tests/e2e.test.ts +112 -28
  28. package/apps/local-cli/tests/tcp.test.ts +71 -19
  29. package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
  30. package/biome.json +1 -0
  31. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  32. package/docs/ADR-002-location-of-authority.md +82 -0
  33. package/docs/ADR-003-durable-object-sharding.md +110 -0
  34. package/docs/ADR-004-dynamodb-schema.md +96 -0
  35. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  36. package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
  37. package/docs/ADR-007-deterministic-ports.md +82 -0
  38. package/docs/ADR-008-monorepo-tooling.md +60 -0
  39. package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
  40. package/docs/ADR-Index.md +15 -0
  41. package/docs/AWS-Adapter-Architecture.md +496 -0
  42. package/docs/AWS-Deployment.md +1275 -0
  43. package/docs/AWS-TCP-Deployment.md +410 -0
  44. package/docs/Cloudflare-Deployment-Guide.md +719 -0
  45. package/docs/Cloudflare-TCP-Deployment.md +510 -0
  46. package/docs/Home.md +15 -0
  47. package/docs/Observability.md +87 -0
  48. package/docs/PlanExtensions.md +812 -0
  49. package/docs/PlanIRCv3Websocket.md +503 -0
  50. package/docs/PlanWebClient.md +451 -0
  51. package/docs/Release-Process.md +443 -0
  52. package/package.json +2 -2
  53. package/packages/aws-adapter/package.json +1 -1
  54. package/packages/aws-adapter/src/handlers/default.ts +3 -0
  55. package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
  56. package/packages/aws-adapter/src/serialize.ts +29 -1
  57. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
  58. package/packages/aws-adapter/tests/account-store.test.ts +4 -7
  59. package/packages/aws-adapter/tests/aws-harness.ts +5 -2
  60. package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
  61. package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
  62. package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
  63. package/packages/aws-adapter/tests/connect.test.ts +9 -9
  64. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
  65. package/packages/aws-adapter/tests/global-setup.ts +55 -30
  66. package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
  67. package/packages/aws-adapter/tests/handlers.test.ts +90 -4
  68. package/packages/aws-adapter/tests/message-store.test.ts +6 -5
  69. package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
  70. package/packages/cf-adapter/package.json +2 -1
  71. package/packages/cf-adapter/src/connection-do.ts +3 -0
  72. package/packages/cf-adapter/src/serialize.ts +25 -4
  73. package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
  74. package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
  75. package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
  76. package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
  77. package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
  78. package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
  79. package/packages/cf-adapter/tests/serialize.test.ts +308 -0
  80. package/packages/cf-adapter/vitest.config.ts +1 -1
  81. package/packages/in-memory-runtime/package.json +1 -1
  82. package/packages/irc-core/package.json +1 -1
  83. package/packages/irc-core/src/caps/capabilities.ts +37 -3
  84. package/packages/irc-core/src/caps/index.ts +1 -0
  85. package/packages/irc-core/src/caps/sts.ts +84 -0
  86. package/packages/irc-core/src/commands/account-notify.ts +53 -0
  87. package/packages/irc-core/src/commands/away.ts +9 -3
  88. package/packages/irc-core/src/commands/cap.ts +23 -5
  89. package/packages/irc-core/src/commands/chathistory.ts +40 -11
  90. package/packages/irc-core/src/commands/index.ts +34 -0
  91. package/packages/irc-core/src/commands/isupport.ts +53 -0
  92. package/packages/irc-core/src/commands/mode.ts +8 -1
  93. package/packages/irc-core/src/commands/monitor.ts +327 -0
  94. package/packages/irc-core/src/commands/multiline.ts +256 -0
  95. package/packages/irc-core/src/commands/pre-away.ts +112 -0
  96. package/packages/irc-core/src/commands/privmsg.ts +19 -12
  97. package/packages/irc-core/src/commands/read-marker.ts +108 -0
  98. package/packages/irc-core/src/commands/registration.ts +8 -0
  99. package/packages/irc-core/src/commands/sasl.ts +24 -1
  100. package/packages/irc-core/src/commands/tagmsg.ts +71 -1
  101. package/packages/irc-core/src/commands/whois.ts +5 -0
  102. package/packages/irc-core/src/config.ts +65 -10
  103. package/packages/irc-core/src/effects.ts +14 -0
  104. package/packages/irc-core/src/ports.ts +389 -0
  105. package/packages/irc-core/src/protocol/index.ts +14 -0
  106. package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
  107. package/packages/irc-core/src/protocol/numerics.ts +15 -0
  108. package/packages/irc-core/src/protocol/outbound.ts +16 -1
  109. package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
  110. package/packages/irc-core/src/state/connection.ts +32 -1
  111. package/packages/irc-core/src/types.ts +112 -0
  112. package/packages/irc-core/stryker.commands.conf.json +1 -2
  113. package/packages/irc-core/tests/away-store.test.ts +73 -0
  114. package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
  115. package/packages/irc-core/tests/caps/sts.test.ts +123 -0
  116. package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
  117. package/packages/irc-core/tests/commands/away.test.ts +85 -2
  118. package/packages/irc-core/tests/commands/cap.test.ts +165 -0
  119. package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
  120. package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
  121. package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
  122. package/packages/irc-core/tests/commands/mode.test.ts +72 -0
  123. package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
  124. package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
  125. package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
  126. package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
  127. package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
  128. package/packages/irc-core/tests/commands/registration.test.ts +64 -0
  129. package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
  130. package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
  131. package/packages/irc-core/tests/commands/whois.test.ts +34 -0
  132. package/packages/irc-core/tests/config.test.ts +103 -13
  133. package/packages/irc-core/tests/labeled-response.test.ts +181 -0
  134. package/packages/irc-core/tests/message-tags.test.ts +114 -0
  135. package/packages/irc-core/tests/monitor-store.test.ts +200 -0
  136. package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
  137. package/packages/irc-core/tests/standard-replies.test.ts +412 -0
  138. package/packages/irc-core/tests/types.test.ts +27 -0
  139. package/packages/irc-server/package.json +1 -1
  140. package/packages/irc-server/src/actor.ts +450 -3
  141. package/packages/irc-server/src/dispatch.ts +108 -16
  142. package/packages/irc-server/tests/actor.test.ts +1432 -39
  143. package/packages/irc-server/tests/dispatch.test.ts +263 -2
  144. package/packages/irc-test-support/package.json +1 -1
  145. package/packages/irc-test-support/src/index.ts +6 -0
  146. package/packages/irc-test-support/src/test-config.ts +54 -0
  147. package/packages/irc-test-support/tests/test-config.test.ts +51 -0
  148. package/tools/ci-hardening/package.json +1 -1
  149. package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
  150. package/tools/load-test/package.json +33 -0
  151. package/tools/load-test/src/client.ts +351 -0
  152. package/tools/load-test/src/config.ts +313 -0
  153. package/tools/load-test/src/harness.ts +116 -0
  154. package/tools/load-test/src/main.ts +120 -0
  155. package/tools/load-test/src/metrics.ts +168 -0
  156. package/tools/load-test/src/report.ts +106 -0
  157. package/tools/load-test/tests/client.test.ts +212 -0
  158. package/tools/load-test/tests/config.test.ts +152 -0
  159. package/tools/load-test/tests/framing.test.ts +37 -0
  160. package/tools/load-test/tests/harness.test.ts +165 -0
  161. package/tools/load-test/tests/metrics.test.ts +174 -0
  162. package/tools/load-test/tests/report.test.ts +161 -0
  163. package/tools/load-test/tests/smoke.test.ts +67 -0
  164. package/tools/load-test/tsconfig.build.json +12 -0
  165. package/tools/load-test/tsconfig.test.json +10 -0
  166. package/tools/load-test/vitest.config.ts +29 -0
  167. package/tools/tcp-ws-forwarder/package.json +1 -1
@@ -17,26 +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),
36
52
  BroadcastWallops: (e, r) => r.broadcastWallops(e.lines, e.except),
37
53
  };
38
54
 
39
- 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
+ }
40
86
 
41
87
  /**
42
88
  * Optional knobs for {@link dispatch}. All observability-related; the
@@ -59,6 +105,16 @@ export interface DispatchOptions {
59
105
  * Stamped on every record alongside `traceId`.
60
106
  */
61
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;
62
118
  }
63
119
 
64
120
  /**
@@ -107,17 +163,22 @@ export async function dispatch(
107
163
  previous = effect;
108
164
  continue;
109
165
  }
110
- await handle(effect, runtime, opts);
166
+ await handle(effect, runtime, opts, opts?.monitor);
111
167
  previous = effect;
112
168
  }
113
169
  }
114
170
 
115
- 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> {
116
177
  // The dispatch table is keyed by tag; the cast is the standard TS pattern
117
178
  // for exhaustive-union dispatch.
118
179
  const handler = handlers[effect.tag] as Handler<Effect>;
119
180
  try {
120
- await handler(effect, runtime);
181
+ await handler(effect, runtime, monitor);
121
182
  } catch (err) {
122
183
  if (opts?.logger !== undefined) {
123
184
  opts.logger.on(logLevelFor(err), 'dispatch.error', {
@@ -175,11 +236,18 @@ function logLevelFor(_err: unknown): LogLevel {
175
236
  * - `cap` without `capLines` (cap-only mode): `lines` go ONLY to
176
237
  * cap-enabled members; the rest receive nothing.
177
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
+ *
178
246
  * `except` is always honored. A member whose connection info is unavailable
179
247
  * (the connection has gone) is treated as lacking the cap.
180
248
  */
181
249
  async function broadcast(e: BroadcastEffect, r: IrcRuntime): Promise<void> {
182
- if (e.cap === undefined) {
250
+ if (e.cap === undefined && e.caps === undefined) {
183
251
  await r.broadcast(e.chan, e.lines, e.except);
184
252
  return;
185
253
  }
@@ -187,16 +255,40 @@ async function broadcast(e: BroadcastEffect, r: IrcRuntime): Promise<void> {
187
255
  const snapshot = await r.getChannelSnapshot(e.chan);
188
256
  if (snapshot === null) return;
189
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];
190
261
  for (const connId of snapshot.members.keys()) {
191
262
  if (connId === e.except) continue;
192
263
 
193
264
  const info = await r.getConnectionInfo(connId);
194
- const hasCap = info?.caps.has(e.cap) ?? false;
265
+ const recipientCaps = info?.caps ?? EMPTY_CAPS;
266
+ const hasAnyCap = capsList.some((c) => recipientCaps.has(c));
195
267
 
268
+ let toSend: RawLine[] | undefined;
196
269
  if (e.capLines !== undefined) {
197
- await r.send(connId, hasCap ? e.capLines : e.lines);
198
- } else if (hasCap) {
199
- 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));
200
276
  }
201
277
  }
202
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>();