serverless-ircd 0.1.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 (165) hide show
  1. package/.github/workflows/ci.yml +47 -0
  2. package/.github/workflows/deploy-cf.yml +97 -0
  3. package/LICENSE +29 -0
  4. package/README.md +323 -0
  5. package/apps/cf-worker/package.json +37 -0
  6. package/apps/cf-worker/scripts/smoke.mjs +175 -0
  7. package/apps/cf-worker/src/worker.ts +59 -0
  8. package/apps/cf-worker/tests/smoke.test.ts +150 -0
  9. package/apps/cf-worker/tsconfig.build.json +17 -0
  10. package/apps/cf-worker/tsconfig.test.json +18 -0
  11. package/apps/cf-worker/vitest.config.ts +31 -0
  12. package/apps/cf-worker/wrangler.test.toml +33 -0
  13. package/apps/cf-worker/wrangler.toml +98 -0
  14. package/apps/local-cli/package.json +35 -0
  15. package/apps/local-cli/src/line-scanner.ts +60 -0
  16. package/apps/local-cli/src/main.ts +145 -0
  17. package/apps/local-cli/src/motd-file.ts +45 -0
  18. package/apps/local-cli/src/server.ts +409 -0
  19. package/apps/local-cli/tests/e2e.test.ts +346 -0
  20. package/apps/local-cli/tests/id-factory.test.ts +25 -0
  21. package/apps/local-cli/tests/line-scanner.test.ts +81 -0
  22. package/apps/local-cli/tests/motd-file.test.ts +71 -0
  23. package/apps/local-cli/tests/tcp.test.ts +358 -0
  24. package/apps/local-cli/tsconfig.build.json +17 -0
  25. package/apps/local-cli/tsconfig.test.json +15 -0
  26. package/apps/local-cli/vitest.config.ts +24 -0
  27. package/biome.json +52 -0
  28. package/package.json +35 -0
  29. package/packages/cf-adapter/package.json +38 -0
  30. package/packages/cf-adapter/src/cf-runtime.ts +308 -0
  31. package/packages/cf-adapter/src/channel-do.ts +374 -0
  32. package/packages/cf-adapter/src/connection-do.ts +542 -0
  33. package/packages/cf-adapter/src/env.ts +67 -0
  34. package/packages/cf-adapter/src/index.ts +38 -0
  35. package/packages/cf-adapter/src/registry-do.ts +130 -0
  36. package/packages/cf-adapter/src/serialize.ts +142 -0
  37. package/packages/cf-adapter/src/sharding.ts +101 -0
  38. package/packages/cf-adapter/tests/cf-harness.ts +264 -0
  39. package/packages/cf-adapter/tests/cf-integration.test.ts +73 -0
  40. package/packages/cf-adapter/tests/cf-runtime.test.ts +527 -0
  41. package/packages/cf-adapter/tests/channel-do.test.ts +480 -0
  42. package/packages/cf-adapter/tests/connection-do.test.ts +329 -0
  43. package/packages/cf-adapter/tests/integration/harness.test.ts +229 -0
  44. package/packages/cf-adapter/tests/integration/harness.ts +102 -0
  45. package/packages/cf-adapter/tests/integration/in-memory-harness.ts +242 -0
  46. package/packages/cf-adapter/tests/integration/index.ts +17 -0
  47. package/packages/cf-adapter/tests/integration/scenarios.test.ts +20 -0
  48. package/packages/cf-adapter/tests/integration/scenarios.ts +495 -0
  49. package/packages/cf-adapter/tests/registry-do.test.ts +335 -0
  50. package/packages/cf-adapter/tests/sharding.test.ts +100 -0
  51. package/packages/cf-adapter/tests/worker/main.ts +33 -0
  52. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +50 -0
  53. package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +57 -0
  54. package/packages/cf-adapter/tsconfig.build.json +16 -0
  55. package/packages/cf-adapter/tsconfig.test.json +18 -0
  56. package/packages/cf-adapter/vitest.config.ts +32 -0
  57. package/packages/cf-adapter/wrangler.test.toml +50 -0
  58. package/packages/in-memory-runtime/package.json +29 -0
  59. package/packages/in-memory-runtime/src/in-memory-runtime.ts +245 -0
  60. package/packages/in-memory-runtime/src/index.ts +9 -0
  61. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +502 -0
  62. package/packages/in-memory-runtime/tsconfig.build.json +16 -0
  63. package/packages/in-memory-runtime/tsconfig.test.json +14 -0
  64. package/packages/in-memory-runtime/vitest.config.ts +21 -0
  65. package/packages/irc-core/package.json +29 -0
  66. package/packages/irc-core/src/caps/capabilities.ts +96 -0
  67. package/packages/irc-core/src/caps/index.ts +1 -0
  68. package/packages/irc-core/src/commands/away.ts +82 -0
  69. package/packages/irc-core/src/commands/cap.ts +257 -0
  70. package/packages/irc-core/src/commands/chghost.ts +30 -0
  71. package/packages/irc-core/src/commands/index.ts +40 -0
  72. package/packages/irc-core/src/commands/invite.ts +156 -0
  73. package/packages/irc-core/src/commands/isupport.ts +133 -0
  74. package/packages/irc-core/src/commands/join.ts +309 -0
  75. package/packages/irc-core/src/commands/kick.ts +162 -0
  76. package/packages/irc-core/src/commands/list.ts +126 -0
  77. package/packages/irc-core/src/commands/mode.ts +655 -0
  78. package/packages/irc-core/src/commands/motd.ts +146 -0
  79. package/packages/irc-core/src/commands/names.ts +164 -0
  80. package/packages/irc-core/src/commands/part.ts +118 -0
  81. package/packages/irc-core/src/commands/ping.ts +47 -0
  82. package/packages/irc-core/src/commands/privmsg.ts +256 -0
  83. package/packages/irc-core/src/commands/quit.ts +70 -0
  84. package/packages/irc-core/src/commands/registration.ts +251 -0
  85. package/packages/irc-core/src/commands/topic.ts +171 -0
  86. package/packages/irc-core/src/commands/who.ts +169 -0
  87. package/packages/irc-core/src/commands/whois.ts +165 -0
  88. package/packages/irc-core/src/effects.ts +184 -0
  89. package/packages/irc-core/src/index.ts +20 -0
  90. package/packages/irc-core/src/ports.ts +153 -0
  91. package/packages/irc-core/src/protocol/batch.ts +85 -0
  92. package/packages/irc-core/src/protocol/index.ts +18 -0
  93. package/packages/irc-core/src/protocol/messages.ts +36 -0
  94. package/packages/irc-core/src/protocol/numerics.ts +155 -0
  95. package/packages/irc-core/src/protocol/outbound.ts +145 -0
  96. package/packages/irc-core/src/protocol/parser.ts +175 -0
  97. package/packages/irc-core/src/protocol/serializer.ts +71 -0
  98. package/packages/irc-core/src/state/channel.ts +293 -0
  99. package/packages/irc-core/src/state/connection.ts +153 -0
  100. package/packages/irc-core/src/state/index.ts +25 -0
  101. package/packages/irc-core/src/state/registry.ts +22 -0
  102. package/packages/irc-core/src/types.ts +83 -0
  103. package/packages/irc-core/tests/batch.test.ts +79 -0
  104. package/packages/irc-core/tests/caps/capabilities.test.ts +148 -0
  105. package/packages/irc-core/tests/commands/away.test.ts +233 -0
  106. package/packages/irc-core/tests/commands/batch.test.ts +178 -0
  107. package/packages/irc-core/tests/commands/cap.test.ts +499 -0
  108. package/packages/irc-core/tests/commands/chghost.test.ts +186 -0
  109. package/packages/irc-core/tests/commands/echo-message.test.ts +212 -0
  110. package/packages/irc-core/tests/commands/extended-join.test.ts +147 -0
  111. package/packages/irc-core/tests/commands/invite-notify.test.ts +160 -0
  112. package/packages/irc-core/tests/commands/invite.test.ts +321 -0
  113. package/packages/irc-core/tests/commands/isupport.test.ts +209 -0
  114. package/packages/irc-core/tests/commands/join.test.ts +687 -0
  115. package/packages/irc-core/tests/commands/kick.test.ts +316 -0
  116. package/packages/irc-core/tests/commands/list.test.ts +452 -0
  117. package/packages/irc-core/tests/commands/mode.test.ts +1048 -0
  118. package/packages/irc-core/tests/commands/motd.test.ts +322 -0
  119. package/packages/irc-core/tests/commands/names.test.ts +342 -0
  120. package/packages/irc-core/tests/commands/part.test.ts +265 -0
  121. package/packages/irc-core/tests/commands/ping.test.ts +144 -0
  122. package/packages/irc-core/tests/commands/privmsg.test.ts +665 -0
  123. package/packages/irc-core/tests/commands/quit.test.ts +220 -0
  124. package/packages/irc-core/tests/commands/registration.test.ts +599 -0
  125. package/packages/irc-core/tests/commands/topic.test.ts +337 -0
  126. package/packages/irc-core/tests/commands/who.test.ts +441 -0
  127. package/packages/irc-core/tests/commands/whois.test.ts +422 -0
  128. package/packages/irc-core/tests/effects.test.ts +147 -0
  129. package/packages/irc-core/tests/message-tags.test.ts +182 -0
  130. package/packages/irc-core/tests/numerics.test.ts +98 -0
  131. package/packages/irc-core/tests/outbound.test.ts +232 -0
  132. package/packages/irc-core/tests/parser.test.ts +162 -0
  133. package/packages/irc-core/tests/ports.test.ts +101 -0
  134. package/packages/irc-core/tests/serializer.test.ts +164 -0
  135. package/packages/irc-core/tests/state/channel.test.ts +351 -0
  136. package/packages/irc-core/tests/state/connection.test.ts +122 -0
  137. package/packages/irc-core/tests/state/registry.test.ts +20 -0
  138. package/packages/irc-core/tests/types.test.ts +127 -0
  139. package/packages/irc-core/tsconfig.build.json +12 -0
  140. package/packages/irc-core/tsconfig.test.json +10 -0
  141. package/packages/irc-core/vitest.config.ts +21 -0
  142. package/packages/irc-server/package.json +28 -0
  143. package/packages/irc-server/src/actor.ts +449 -0
  144. package/packages/irc-server/src/dispatch.ts +120 -0
  145. package/packages/irc-server/src/index.ts +11 -0
  146. package/packages/irc-server/src/runtime.ts +61 -0
  147. package/packages/irc-server/tests/actor.test.ts +816 -0
  148. package/packages/irc-server/tests/dispatch.test.ts +512 -0
  149. package/packages/irc-server/tests/runtime.test.ts +52 -0
  150. package/packages/irc-server/tsconfig.build.json +13 -0
  151. package/packages/irc-server/tsconfig.test.json +11 -0
  152. package/packages/irc-server/vitest.config.ts +21 -0
  153. package/pnpm-workspace.yaml +4 -0
  154. package/tools/tcp-ws-forwarder/package.json +32 -0
  155. package/tools/tcp-ws-forwarder/src/forwarder.ts +244 -0
  156. package/tools/tcp-ws-forwarder/src/line-scanner.ts +55 -0
  157. package/tools/tcp-ws-forwarder/src/main.ts +114 -0
  158. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +618 -0
  159. package/tools/tcp-ws-forwarder/tests/framing.test.ts +51 -0
  160. package/tools/tcp-ws-forwarder/tests/line-scanner.test.ts +75 -0
  161. package/tools/tcp-ws-forwarder/tsconfig.build.json +12 -0
  162. package/tools/tcp-ws-forwarder/tsconfig.test.json +10 -0
  163. package/tools/tcp-ws-forwarder/vitest.config.ts +27 -0
  164. package/tsconfig.base.json +25 -0
  165. package/turbo.json +29 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * In-memory {@link IrcHarness} implementation.
3
+ *
4
+ * Wires {@link InMemoryRuntime} + {@link ConnectionActor} into the
5
+ * harness seam. Each {@link InMemoryHarness} instance is hermetic: fresh
6
+ * `Map`s, fresh clock, fresh id counter. Tests construct one per `it(...)`
7
+ * via {@link inMemoryHarnessFactory} and dispose with {@link close}.
8
+ *
9
+ * Because the in-memory runtime's send/broadcast/disconnect handlers are
10
+ * synchronous, by the time `await client.send(line)` resolves every side
11
+ * effect on every other client's `received` array has already landed.
12
+ * {@link InMemoryClientHarness.waitForLine} still polls on a short timer
13
+ * so test code can race `await Promise.all([a.waitForLine(...), b.send(...)])`
14
+ * without depending on that synchrony — keeps the same scenario code honest
15
+ * when it runs against the genuinely-async CF / AWS runtimes.
16
+ */
17
+
18
+ import { InMemoryRuntime } from '@serverless-ircd/in-memory-runtime';
19
+ import {
20
+ type ConnId,
21
+ FakeClock,
22
+ type RawLine,
23
+ SequentialIdFactory,
24
+ StaticMotdProvider,
25
+ createConnection,
26
+ } from '@serverless-ircd/irc-core';
27
+ import { ConnectionActor } from '@serverless-ircd/irc-server';
28
+ import type {
29
+ ClientHarness,
30
+ IrcHarness,
31
+ IrcHarnessFactory,
32
+ SpawnClientOptions,
33
+ } from './harness.js';
34
+
35
+ /** Default tick for the polling waitFor* implementations (ms). */
36
+ const POLL_MS = 10;
37
+
38
+ /** Default timeout for waitFor* implementations (ms). */
39
+ const DEFAULT_TIMEOUT_MS = 1000;
40
+
41
+ /** Shared server config used by the harness. Hard-coded — tests assert on it. */
42
+ export const HARNESS_SERVER_CONFIG = {
43
+ serverName: 'irc.example.com',
44
+ networkName: 'ExampleNet',
45
+ maxChannelsPerUser: 30,
46
+ maxTargetsPerCommand: 10,
47
+ maxListEntries: 50,
48
+ nickLen: 30,
49
+ channelLen: 50,
50
+ topicLen: 390,
51
+ quitMessage: 'Client Quit',
52
+ } as const;
53
+
54
+ /**
55
+ * Default MOTD lines exposed to every spawned client. The welcome block
56
+ * always emits the placeholder 375/372/376 numerics; an explicit `MOTD`
57
+ * command after registration reads these lines via the {@link MotdProvider}
58
+ * port. Tests assert against this content (scenario: MOTD).
59
+ */
60
+ const HARNESS_MOTD_LINES = [
61
+ 'Welcome to the ServerlessIRCd integration-test harness.',
62
+ 'All scenarios run parametrized over the IrcRuntime factory matrix.',
63
+ ];
64
+
65
+ /**
66
+ * Factory entry for the in-memory runtime. Append this to a suite's
67
+ * FACTORIES array to opt the scenarios into the in-memory matrix.
68
+ */
69
+ export const inMemoryHarnessFactory: IrcHarnessFactory = Object.freeze({
70
+ name: 'in-memory',
71
+ async create() {
72
+ return new InMemoryHarness();
73
+ },
74
+ });
75
+
76
+ interface TrackedClient {
77
+ harness: InMemoryClientHarness;
78
+ disconnect: () => void;
79
+ }
80
+
81
+ export class InMemoryHarness implements IrcHarness {
82
+ private readonly clock = new FakeClock(0);
83
+ private readonly runtime = new InMemoryRuntime({ clock: this.clock });
84
+ private readonly ids = new SequentialIdFactory();
85
+ private readonly motd = new StaticMotdProvider(HARNESS_MOTD_LINES);
86
+ private readonly clients = new Map<ConnId, TrackedClient>();
87
+ private nextId = 0;
88
+ private closed = false;
89
+
90
+ async spawnClient(opts?: SpawnClientOptions): Promise<ClientHarness> {
91
+ if (this.closed) throw new Error('spawnClient called on a closed harness');
92
+ const id = opts?.id ?? `c${this.nextId++}`;
93
+ if (this.clients.has(id)) {
94
+ throw new Error(`duplicate client id: ${id}`);
95
+ }
96
+ const state = createConnection({ id, connectedSince: this.clock.now() });
97
+
98
+ const client = new InMemoryClientHarness(id);
99
+ let disconnected = false;
100
+ const disconnect = () => {
101
+ if (disconnected) return;
102
+ disconnected = true;
103
+ this.runtime.unregisterConnection(id);
104
+ };
105
+
106
+ this.runtime.registerConnection(state, {
107
+ send: (lines: RawLine[]) => {
108
+ for (const l of lines) client.pushReceived(l.text);
109
+ },
110
+ disconnect: () => disconnect(),
111
+ });
112
+
113
+ const actor = new ConnectionActor({
114
+ state,
115
+ runtime: this.runtime,
116
+ channels: this.runtime,
117
+ serverConfig: HARNESS_SERVER_CONFIG,
118
+ clock: this.clock,
119
+ ids: this.ids,
120
+ motd: this.motd,
121
+ });
122
+ client.bindActor(actor);
123
+
124
+ this.clients.set(id, { harness: client, disconnect });
125
+
126
+ if (opts?.nick !== undefined) {
127
+ await client.send(`NICK ${opts.nick}`);
128
+ await client.send(`USER ${opts.nick} 0 * :${opts.nick}`);
129
+ // Welcome fires synchronously through the in-memory runtime's
130
+ // send handler; by the time send() resolves, the 001 is in
131
+ // received[]. Await it (with a small timeout safety net) so the
132
+ // caller can rely on a registered client on return.
133
+ await client.waitForNumeric('001', DEFAULT_TIMEOUT_MS);
134
+ }
135
+ return client;
136
+ }
137
+
138
+ async clientByNick(nick: string): Promise<ClientHarness | undefined> {
139
+ const connId = await this.runtime.lookupNick(nick);
140
+ if (connId === null) return undefined;
141
+ return this.clients.get(connId)?.harness;
142
+ }
143
+
144
+ async close(): Promise<void> {
145
+ if (this.closed) return;
146
+ this.closed = true;
147
+ for (const { disconnect } of this.clients.values()) disconnect();
148
+ this.clients.clear();
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Per-connection harness. The actor is bound post-construction via
154
+ * {@link bindActor} so the constructor can register itself with the
155
+ * runtime before the actor is wired up (the runtime's `send` handler
156
+ * closes over `client.pushReceived`).
157
+ */
158
+ class InMemoryClientHarness implements ClientHarness {
159
+ readonly received: string[] = [];
160
+ private actor: ConnectionActor | undefined;
161
+ private closed = false;
162
+
163
+ constructor(readonly id: ConnId) {}
164
+
165
+ /** Late-bound by the harness once the runtime is registered. */
166
+ bindActor(actor: ConnectionActor): void {
167
+ this.actor = actor;
168
+ }
169
+
170
+ pushReceived(text: string): void {
171
+ this.received.push(text);
172
+ }
173
+
174
+ async send(line: string): Promise<void> {
175
+ /* istanbul ignore next -- defensive: bindActor is called by the
176
+ harness before the client is returned, so the unbound-actor branch
177
+ is unreachable through the public API. */
178
+ if (this.actor === undefined) {
179
+ throw new Error(`client ${this.id} has no bound actor`);
180
+ }
181
+ await this.actor.receiveTextFrame(`${line}\r\n`);
182
+ }
183
+
184
+ async waitForLine(
185
+ predicate: (line: string) => boolean,
186
+ timeoutMs: number = DEFAULT_TIMEOUT_MS,
187
+ ): Promise<string> {
188
+ const deadline = Date.now() + timeoutMs;
189
+ // Scan already-received lines first.
190
+ for (const line of this.received) {
191
+ if (predicate(line)) return line;
192
+ }
193
+ while (Date.now() < deadline) {
194
+ await new Promise((r) => setTimeout(r, POLL_MS));
195
+ for (const line of this.received) {
196
+ if (predicate(line)) return line;
197
+ }
198
+ }
199
+ throw new Error(
200
+ `waitForLine timed out after ${timeoutMs}ms; received: ${JSON.stringify(this.received)}`,
201
+ );
202
+ }
203
+
204
+ async waitForNumeric(
205
+ code: number | string,
206
+ timeoutMs: number = DEFAULT_TIMEOUT_MS,
207
+ ): Promise<string> {
208
+ const want = typeof code === 'number' ? code.toString().padStart(3, '0') : code;
209
+ return this.waitForLine((l) => numericEquals(l, want), timeoutMs);
210
+ }
211
+
212
+ async close(): Promise<void> {
213
+ if (this.closed) return;
214
+ this.closed = true;
215
+ // Best-effort QUIT so peers see the part broadcast; the runtime's
216
+ // disconnect handler is wired by the harness to unregister.
217
+ if (this.actor !== undefined) {
218
+ try {
219
+ await this.actor.receiveTextFrame('QUIT :client closed\r\n');
220
+ /* istanbul ignore next -- defensive: receiveTextFrame swallows
221
+ every error internally; the catch is purely a teardown safety
222
+ net for catastrophic runtime failures. */
223
+ } catch {
224
+ // no-op
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Returns true iff the second whitespace-delimited token of `line` is
232
+ * `code` (after the optional `:server` prefix). Matches both
233
+ * `:irc.example.com 001 nick ...` and the bare `PONG :tok` form (which
234
+ * has no numeric and therefore does not match).
235
+ */
236
+ function numericEquals(line: string, code: string): boolean {
237
+ const parts = line.split(' ');
238
+ // Skip the optional `:prefix` token.
239
+ const start = parts[0]?.startsWith(':') ? 1 : 0;
240
+ const candidate = parts[start];
241
+ return candidate === code;
242
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * In-tree parametrized IRC scenario suite (folded in from the former
3
+ * `@serverless-ircd/integration-tests` workspace package).
4
+ *
5
+ * Each runtime (in-memory now; CF via `../cf-harness.ts`; AWS in Phase 4)
6
+ * ships one {@link IrcHarnessFactory}; the scenario tests run against
7
+ * every registered factory.
8
+ */
9
+
10
+ export type {
11
+ ClientHarness,
12
+ IrcHarness,
13
+ IrcHarnessFactory,
14
+ SpawnClientOptions,
15
+ } from './harness.js';
16
+ export { inMemoryHarnessFactory, HARNESS_SERVER_CONFIG } from './in-memory-harness.js';
17
+ export { runIrcScenarios } from './scenarios.js';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Parametrized IRC scenarios — 032.
3
+ *
4
+ * The scenario bodies live in `src/scenarios.ts` (now re-exported as
5
+ * `runIrcScenarios`) so adapter packages can register their own
6
+ * {@link IrcHarnessFactory} against the exact same suite under their
7
+ * native vitest configuration (CF runs in `workerd`, AWS runs in node
8
+ * with dynamodb-local, etc.). This file is the in-memory entry point —
9
+ * it registers only {@link inMemoryHarnessFactory}.
10
+ *
11
+ * Adding a runtime to the in-memory suite = appending one entry to the
12
+ * `FACTORIES` array (acceptance criterion: "adding a new runtime
13
+ * requires only registering a factory").
14
+ */
15
+
16
+ import { inMemoryHarnessFactory } from './in-memory-harness.js';
17
+ import { runIrcScenarios } from './scenarios.js';
18
+
19
+ /** The runtime matrix. Adding a runtime = appending one entry here. */
20
+ runIrcScenarios([inMemoryHarnessFactory]);