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.
- package/.github/workflows/ci.yml +34 -2
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/CHANGELOG.md +435 -0
- package/README.md +206 -27
- package/apps/aws-stack/README.md +37 -3
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +106 -13
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +49 -3
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.toml +5 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +138 -28
- package/apps/local-cli/tests/e2e.test.ts +113 -29
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +262 -0
- package/biome.json +1 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +15 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +69 -0
- package/packages/aws-adapter/src/handlers/connect.ts +36 -5
- package/packages/aws-adapter/src/handlers/default.ts +66 -5
- package/packages/aws-adapter/src/handlers/index.ts +41 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +18 -0
- package/packages/aws-adapter/src/index.ts +2 -0
- package/packages/aws-adapter/src/serialize.ts +40 -2
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +140 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +100 -4
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +238 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +62 -8
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/cf-adapter/package.json +6 -1
- package/packages/cf-adapter/src/cf-runtime.ts +66 -1
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/connection-do.ts +185 -54
- package/packages/cf-adapter/src/env.ts +25 -6
- package/packages/cf-adapter/src/index.ts +2 -0
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +1 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +4 -4
- package/packages/cf-adapter/tests/cf-runtime.test.ts +307 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +119 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +108 -4
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +7 -7
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/cf-adapter/wrangler.test.toml +7 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +38 -4
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +42 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/isupport.ts +59 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +12 -9
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +72 -10
- package/packages/irc-core/src/effects.ts +41 -1
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +568 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +57 -11
- package/packages/irc-core/src/protocol/outbound.ts +36 -4
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +120 -1
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +129 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +90 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +699 -19
- package/packages/irc-server/src/dispatch.ts +109 -16
- package/packages/irc-server/src/routing.ts +3 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +10 -7
- package/packages/irc-server/tests/actor.test.ts +2523 -42
- package/packages/irc-server/tests/dispatch.test.ts +300 -2
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +1 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +9 -1
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/package.json +6 -1
- package/tools/seed-cf-accounts.ts +4 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* • a `ws.WebSocketServer` on {@link StartServerOptions.port} — the
|
|
9
9
|
* original WebSocket transport (one IRC message per text frame, or N
|
|
10
10
|
* `\r\n`-joined messages per frame). Programmatic test clients use this.
|
|
11
|
+
* When a client negotiates an IRCv3 subprotocol
|
|
12
|
+
* (`text.ircv3.net` / `binary.ircv3.net`) the connection switches to
|
|
13
|
+
* spec framing: one IRC message per WebSocket message with no trailing
|
|
14
|
+
* CR-LF, a 510-byte inbound budget, and (for text) lone-surrogate
|
|
15
|
+
* sanitization. Un-negotiated clients keep the legacy framing.
|
|
11
16
|
* • an optional `node:net` TCP server on {@link StartServerOptions.tcpPort}
|
|
12
17
|
* — the RFC-style byte stream that real IRC clients (WeeChat, HexChat,
|
|
13
18
|
* IRCCloud) speak. The TCP path uses {@link LineScanner} to reassemble
|
|
@@ -41,9 +46,19 @@ import {
|
|
|
41
46
|
type MessageStore,
|
|
42
47
|
type NickHistoryStore,
|
|
43
48
|
type RawLine,
|
|
49
|
+
type ServerConfig,
|
|
50
|
+
WS_SUBPROTO_BINARY,
|
|
51
|
+
WS_SUBPROTO_TEXT,
|
|
52
|
+
type WsFrameMode,
|
|
53
|
+
type WsSubprotocol,
|
|
44
54
|
createConnection,
|
|
55
|
+
frameToLines,
|
|
56
|
+
isWithinWsByteBudget,
|
|
57
|
+
sanitizeForTextMode,
|
|
58
|
+
selectSubprotocol,
|
|
59
|
+
wsFrameModeFor,
|
|
45
60
|
} from '@serverless-ircd/irc-core';
|
|
46
|
-
import { ConnectionActor } from '@serverless-ircd/irc-server';
|
|
61
|
+
import { ConnectionActor, type Transport } from '@serverless-ircd/irc-server';
|
|
47
62
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
48
63
|
import { loadServerConfigFromCliArgs } from './config-loader.js';
|
|
49
64
|
import { LineScanner } from './line-scanner.js';
|
|
@@ -73,6 +88,28 @@ type ResolvedServerConfig = Omit<
|
|
|
73
88
|
| 'saslAccounts'
|
|
74
89
|
>;
|
|
75
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Builds the reducer-facing {@link ServerConfig} from the resolved local-cli
|
|
93
|
+
* config. Shared by {@link attachConnection} (initial config) and the
|
|
94
|
+
* `InMemoryRuntime` reload source (REHASH) so a reload returns the same
|
|
95
|
+
* shape the actor was bootstrapped with — keeping the two paths consistent.
|
|
96
|
+
*/
|
|
97
|
+
function serverConfigFromResolved(cfg: ResolvedServerConfig): ServerConfig {
|
|
98
|
+
return {
|
|
99
|
+
serverName: cfg.serverName,
|
|
100
|
+
networkName: cfg.networkName,
|
|
101
|
+
maxChannelsPerUser: cfg.maxChannelsPerUser,
|
|
102
|
+
maxTargetsPerCommand: cfg.maxTargetsPerCommand,
|
|
103
|
+
maxListEntries: cfg.maxListEntries,
|
|
104
|
+
nickLen: cfg.nickLen,
|
|
105
|
+
channelLen: cfg.channelLen,
|
|
106
|
+
topicLen: cfg.topicLen,
|
|
107
|
+
quitMessage: cfg.quitMessage,
|
|
108
|
+
...(cfg.serverPassword !== undefined ? { serverPassword: cfg.serverPassword } : {}),
|
|
109
|
+
...(cfg.cloaking !== undefined ? { cloaking: cfg.cloaking } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
/** Default per-IP / per-user caps used when callers don't override. */
|
|
77
114
|
const DEFAULT_MAX_CONNECTIONS_PER_IP = 10;
|
|
78
115
|
const DEFAULT_MAX_CONNECTIONS_PER_USER = 5;
|
|
@@ -198,9 +235,9 @@ const DEFAULT_CONFIG: Omit<
|
|
|
198
235
|
| 'perIpConnectionRate'
|
|
199
236
|
| 'historyMaxPerChannel'
|
|
200
237
|
| 'saslAccounts'
|
|
238
|
+
| 'serverName'
|
|
239
|
+
| 'networkName'
|
|
201
240
|
> = {
|
|
202
|
-
serverName: 'irc.example.com',
|
|
203
|
-
networkName: 'ExampleNet',
|
|
204
241
|
motdLines: ['Welcome to the local-cli ServerlessIRCd instance.'],
|
|
205
242
|
maxChannelsPerUser: 30,
|
|
206
243
|
maxTargetsPerCommand: 10,
|
|
@@ -216,7 +253,7 @@ const DEFAULT_CONFIG: Omit<
|
|
|
216
253
|
* the three generators once at module load, not per-connection. No reducer in
|
|
217
254
|
* the current codebase actually invokes `batchId` / `nonce` / `sessionId`;
|
|
218
255
|
* they are forward-looking seams for SASL / BATCH / context-restoration
|
|
219
|
-
*
|
|
256
|
+
* and are exercised by irc-core's own unit tests via test fakes.
|
|
220
257
|
*
|
|
221
258
|
* Exported so `apps/local-cli` can ship a focused unit test asserting each
|
|
222
259
|
* generator returns a fresh UUID-shaped string (defending against future
|
|
@@ -242,14 +279,16 @@ export function startLocalServer(opts: StartServerOptions): Promise<LocalServer>
|
|
|
242
279
|
// Validate the user-supplied config through the shared ServerConfigSchema
|
|
243
280
|
// (sourced from `irc-core`). Both adapters consume the same schema; here
|
|
244
281
|
// we discard the parsed result and only retain the side-effect of
|
|
245
|
-
// throwing on a malformed config at boot.
|
|
246
|
-
//
|
|
247
|
-
//
|
|
282
|
+
// throwing on a malformed config at boot. `serverName` and `networkName`
|
|
283
|
+
// are required by the schema — passing them through as `undefined` when
|
|
284
|
+
// the caller omits them lets the loader fail fast with a readable error
|
|
285
|
+
// pointing at the missing knob, so the server never boots advertising a
|
|
286
|
+
// placeholder identity.
|
|
248
287
|
let parsed: ReturnType<typeof loadServerConfigFromCliArgs>;
|
|
249
288
|
try {
|
|
250
289
|
parsed = loadServerConfigFromCliArgs({
|
|
251
|
-
serverName: opts.serverName
|
|
252
|
-
networkName: opts.networkName
|
|
290
|
+
serverName: opts.serverName,
|
|
291
|
+
networkName: opts.networkName,
|
|
253
292
|
...(opts.motdLines !== undefined ? { motdLines: opts.motdLines } : {}),
|
|
254
293
|
...(opts.maxChannelsPerUser !== undefined
|
|
255
294
|
? { maxChannelsPerUser: opts.maxChannelsPerUser }
|
|
@@ -312,6 +351,10 @@ export function startLocalServer(opts: StartServerOptions): Promise<LocalServer>
|
|
|
312
351
|
const runtime = new InMemoryRuntime({
|
|
313
352
|
clock: { now: () => Date.now() },
|
|
314
353
|
...(admission !== undefined ? { admission } : {}),
|
|
354
|
+
// REHASH reload source: re-derive the live ServerConfig from the
|
|
355
|
+
// resolved config so an oper's `REHASH` re-fetches the latest values
|
|
356
|
+
// (rotated oper creds, MOTD, limits) without restarting the process.
|
|
357
|
+
configLoader: async () => serverConfigFromResolved(cfg),
|
|
315
358
|
});
|
|
316
359
|
|
|
317
360
|
// Per-process chat-history store: one ring buffer shared by every
|
|
@@ -333,8 +376,23 @@ export function startLocalServer(opts: StartServerOptions): Promise<LocalServer>
|
|
|
333
376
|
? new InMemoryAccountStore(cfg.saslAccounts)
|
|
334
377
|
: undefined;
|
|
335
378
|
|
|
336
|
-
const wss = new WebSocketServer({
|
|
379
|
+
const wss = new WebSocketServer({
|
|
380
|
+
port: opts.port,
|
|
381
|
+
host: hostname,
|
|
382
|
+
// IRCv3 WebSocket subprotocol negotiation: select the first supported
|
|
383
|
+
// offer in client-preference order (core selector), or fall through to
|
|
384
|
+
// legacy framing when none match. The `ws` library only invokes this
|
|
385
|
+
// when the client offered protocols; returning the selected name echoes
|
|
386
|
+
// it, returning false completes the handshake with no
|
|
387
|
+
// Sec-WebSocket-Protocol header (legacy) — it never rejects.
|
|
388
|
+
handleProtocols: (protocols: Set<string>): string | false =>
|
|
389
|
+
selectSubprotocol([...protocols]) ?? false,
|
|
390
|
+
});
|
|
337
391
|
const wsConnections = new Map<WebSocket, ConnectionBindings>();
|
|
392
|
+
// Per-connection negotiated IRCv3 subprotocol registry (null = legacy
|
|
393
|
+
// framing). Mirrors the selection made in `handleProtocols`; the resolved
|
|
394
|
+
// frame mode is also captured in the per-connection handlers below.
|
|
395
|
+
const wsModes = new Map<WebSocket, WsSubprotocol | null>();
|
|
338
396
|
|
|
339
397
|
wss.on('connection', (ws, req) => {
|
|
340
398
|
// Admission gate: refuse the connection BEFORE we attach an actor / emit
|
|
@@ -350,18 +408,50 @@ export function startLocalServer(opts: StartServerOptions): Promise<LocalServer>
|
|
|
350
408
|
const admissionRecordId = decision.recordId;
|
|
351
409
|
runtime.commitAdmission(ip, undefined, admissionRecordId);
|
|
352
410
|
|
|
411
|
+
// Resolve the negotiated IRCv3 subprotocol (echoed by handleProtocols)
|
|
412
|
+
// and derive the frame mode. `ws.protocol` is '' when none negotiated.
|
|
413
|
+
const subproto: WsSubprotocol | null =
|
|
414
|
+
ws.protocol === WS_SUBPROTO_TEXT || ws.protocol === WS_SUBPROTO_BINARY
|
|
415
|
+
? (ws.protocol as WsSubprotocol)
|
|
416
|
+
: null;
|
|
417
|
+
wsModes.set(ws, subproto);
|
|
418
|
+
const frameMode = wsFrameModeFor(subproto ?? undefined);
|
|
419
|
+
|
|
353
420
|
const { state, actor } = attachConnection(runtime, cfg, messages, accounts, history, {
|
|
354
421
|
sendText: (text) => {
|
|
355
|
-
if (ws.readyState
|
|
422
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
423
|
+
// binary.ircv3.net connections exchange binary WebSocket frames;
|
|
424
|
+
// text/legacy connections send UTF-8 text frames.
|
|
425
|
+
ws.send(frameMode === 'spec-binary' ? Buffer.from(text, 'utf8') : text);
|
|
356
426
|
},
|
|
357
427
|
closeTransport: () => ws.close(),
|
|
358
428
|
admissionRecordId,
|
|
359
429
|
sourceHost: ip,
|
|
430
|
+
frameMode,
|
|
431
|
+
// Spec-mode framing: one IRC message per WS message (strip a single
|
|
432
|
+
// trailing CR-LF, never split on an embedded line break). Legacy
|
|
433
|
+
// connections fall through to the actor's default WsTextFrameTransport.
|
|
434
|
+
...(frameMode === 'legacy'
|
|
435
|
+
? {}
|
|
436
|
+
: { actorTransport: { feed: (chunk: string) => frameToLines(chunk, frameMode) } }),
|
|
360
437
|
});
|
|
361
438
|
wsConnections.set(ws, { state, actor });
|
|
362
439
|
|
|
363
440
|
ws.on('message', (data) => {
|
|
364
441
|
const text = data.toString('utf8');
|
|
442
|
+
// Spec-mode inbound: enforce the IRCv3 510-byte message budget and
|
|
443
|
+
// sanitize lone surrogates for text frames before framing. Legacy
|
|
444
|
+
// frames pass through unchanged (the 512-byte line cap is enforced in
|
|
445
|
+
// the parser, as before).
|
|
446
|
+
if (frameMode === 'spec-text' || frameMode === 'spec-binary') {
|
|
447
|
+
if (!isWithinWsByteBudget(text)) {
|
|
448
|
+
ws.close(1009, 'Message Too Big');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const inbound = frameMode === 'spec-text' ? sanitizeForTextMode(text) : text;
|
|
452
|
+
actor.receiveTextFrame(inbound).catch((err) => logError('actor failure', err));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
365
455
|
// Fire-and-forget: the actor's reducer pipeline is synchronous through
|
|
366
456
|
// to dispatch, and dispatch awaits each runtime call in order. Errors
|
|
367
457
|
// propagate to the catch here and are logged (never crash the server).
|
|
@@ -376,6 +466,7 @@ export function startLocalServer(opts: StartServerOptions): Promise<LocalServer>
|
|
|
376
466
|
// admission record (if any) so per-IP / per-user counters stay accurate.
|
|
377
467
|
const cleanup = (): void => {
|
|
378
468
|
wsConnections.delete(ws);
|
|
469
|
+
wsModes.delete(ws);
|
|
379
470
|
runtime.unregisterConnection(state.id);
|
|
380
471
|
};
|
|
381
472
|
ws.on('close', cleanup);
|
|
@@ -527,6 +618,20 @@ function attachConnection(
|
|
|
527
618
|
closeTransport: () => void;
|
|
528
619
|
admissionRecordId?: string;
|
|
529
620
|
sourceHost?: string;
|
|
621
|
+
/**
|
|
622
|
+
* Negotiated IRCv3 WebSocket frame mode for this connection. Spec modes
|
|
623
|
+
* (`spec-text` / `spec-binary`) switch outbound delivery to one WS
|
|
624
|
+
* message per IRC line with no trailing CR-LF (and surrogate
|
|
625
|
+
* sanitization for text). `undefined` / `legacy` keeps the original
|
|
626
|
+
* `\r\n`-joined batch. TCP connections never set this.
|
|
627
|
+
*/
|
|
628
|
+
frameMode?: WsFrameMode;
|
|
629
|
+
/**
|
|
630
|
+
* Inbound line-framing seam forwarded to the actor's `Transport`. Only
|
|
631
|
+
* set for spec-mode WebSocket connections (one IRC line per frame);
|
|
632
|
+
* when omitted the actor defaults to its WS-text-frame transport.
|
|
633
|
+
*/
|
|
634
|
+
actorTransport?: Transport;
|
|
530
635
|
},
|
|
531
636
|
): ConnectionBindings {
|
|
532
637
|
const id = randomUUID();
|
|
@@ -547,19 +652,8 @@ function attachConnection(
|
|
|
547
652
|
state,
|
|
548
653
|
runtime,
|
|
549
654
|
channels: runtime,
|
|
550
|
-
serverConfig:
|
|
551
|
-
|
|
552
|
-
networkName: cfg.networkName,
|
|
553
|
-
maxChannelsPerUser: cfg.maxChannelsPerUser,
|
|
554
|
-
maxTargetsPerCommand: cfg.maxTargetsPerCommand,
|
|
555
|
-
maxListEntries: cfg.maxListEntries,
|
|
556
|
-
nickLen: cfg.nickLen,
|
|
557
|
-
channelLen: cfg.channelLen,
|
|
558
|
-
topicLen: cfg.topicLen,
|
|
559
|
-
quitMessage: cfg.quitMessage,
|
|
560
|
-
...(cfg.serverPassword !== undefined ? { serverPassword: cfg.serverPassword } : {}),
|
|
561
|
-
...(cfg.cloaking !== undefined ? { cloaking: cfg.cloaking } : {}),
|
|
562
|
-
},
|
|
655
|
+
serverConfig: serverConfigFromResolved(cfg),
|
|
656
|
+
configSource: 'config file',
|
|
563
657
|
clock: { now: () => Date.now() },
|
|
564
658
|
ids: DEFAULT_ID_FACTORY,
|
|
565
659
|
motd: { lines: () => cfg.motdLines },
|
|
@@ -567,21 +661,37 @@ function attachConnection(
|
|
|
567
661
|
...(accounts !== undefined ? { accounts } : {}),
|
|
568
662
|
history,
|
|
569
663
|
logger,
|
|
664
|
+
...(transport.actorTransport !== undefined ? { transport: transport.actorTransport } : {}),
|
|
570
665
|
});
|
|
571
666
|
|
|
572
667
|
runtime.registerConnection(
|
|
573
668
|
state,
|
|
574
669
|
{
|
|
575
670
|
send: (lines: RawLine[]) => {
|
|
576
|
-
const
|
|
577
|
-
|
|
671
|
+
const fm = transport.frameMode;
|
|
672
|
+
if (fm === 'spec-text' || fm === 'spec-binary') {
|
|
673
|
+
// Spec mode: one WebSocket message per IRC line, no trailing
|
|
674
|
+
// CR-LF. Sanitize lone surrogates for text frames (they have no
|
|
675
|
+
// valid UTF-8 encoding); binary frames pass through verbatim.
|
|
676
|
+
for (const l of lines) {
|
|
677
|
+
transport.sendText(fm === 'spec-text' ? sanitizeForTextMode(l.text) : l.text);
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
const text = `${lines.map((l) => l.text).join('\r\n')}\r\n`;
|
|
681
|
+
transport.sendText(text);
|
|
682
|
+
}
|
|
578
683
|
},
|
|
579
684
|
disconnect: (reason?: string) => {
|
|
580
685
|
// If a reason was supplied, send the RFC-style ERROR notice before
|
|
581
686
|
// closing. (QUIT does not supply one; future paths like flood
|
|
582
|
-
// control will.)
|
|
687
|
+
// control will.) In spec mode the notice is its own message with no
|
|
688
|
+
// trailing CR-LF.
|
|
583
689
|
if (reason !== undefined) {
|
|
584
|
-
|
|
690
|
+
const line = `ERROR :Closing link: (${reason})`;
|
|
691
|
+
const fm = transport.frameMode;
|
|
692
|
+
if (fm === 'spec-text') transport.sendText(sanitizeForTextMode(line));
|
|
693
|
+
else if (fm === 'spec-binary') transport.sendText(line);
|
|
694
|
+
else transport.sendText(`${line}\r\n`);
|
|
585
695
|
}
|
|
586
696
|
transport.closeTransport();
|
|
587
697
|
},
|
|
@@ -80,7 +80,12 @@ describe('local-cli e2e', () => {
|
|
|
80
80
|
let server: LocalServer;
|
|
81
81
|
|
|
82
82
|
beforeAll(async () => {
|
|
83
|
-
server = await startLocalServer({
|
|
83
|
+
server = await startLocalServer({
|
|
84
|
+
port: 0,
|
|
85
|
+
hostname: '127.0.0.1',
|
|
86
|
+
serverName: 'irc.local.example.com',
|
|
87
|
+
networkName: 'LocalNet',
|
|
88
|
+
});
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
afterAll(async () => {
|
|
@@ -103,15 +108,15 @@ describe('local-cli e2e', () => {
|
|
|
103
108
|
|
|
104
109
|
await alice.send('NICK alice');
|
|
105
110
|
await alice.send('USER alice 0 * :Alice');
|
|
106
|
-
await alice.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
111
|
+
await alice.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
107
112
|
|
|
108
113
|
await bob.send('NICK bob');
|
|
109
114
|
await bob.send('USER bob 0 * :Bob');
|
|
110
|
-
await bob.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
115
|
+
await bob.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
111
116
|
|
|
112
117
|
await alice.send('JOIN #e2e');
|
|
113
118
|
await alice.waitFor((l) => l.includes('JOIN #e2e'));
|
|
114
|
-
await alice.waitFor((l) => l.startsWith(':irc.example.com 353 '));
|
|
119
|
+
await alice.waitFor((l) => l.startsWith(':irc.local.example.com 353 '));
|
|
115
120
|
|
|
116
121
|
await bob.send('JOIN #e2e');
|
|
117
122
|
// Bob sees his own JOIN + NAMES.
|
|
@@ -130,7 +135,7 @@ describe('local-cli e2e', () => {
|
|
|
130
135
|
await client.opened();
|
|
131
136
|
await client.send('NICK charlie');
|
|
132
137
|
await client.send('USER charlie 0 * :Charlie');
|
|
133
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
138
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
134
139
|
|
|
135
140
|
await client.send('PING :tok-12345');
|
|
136
141
|
await client.waitFor((l) => l === 'PONG :tok-12345');
|
|
@@ -143,7 +148,7 @@ describe('local-cli e2e', () => {
|
|
|
143
148
|
await client.opened();
|
|
144
149
|
await client.send('NICK dave');
|
|
145
150
|
await client.send('USER dave 0 * :Dave');
|
|
146
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
151
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
147
152
|
|
|
148
153
|
await client.send('BOGUSCMD');
|
|
149
154
|
await client.waitFor((l) => l.includes('421') && l.includes('BOGUSCMD'));
|
|
@@ -156,7 +161,7 @@ describe('local-cli e2e', () => {
|
|
|
156
161
|
await client.opened();
|
|
157
162
|
await client.send('NICK eve');
|
|
158
163
|
await client.send('USER eve 0 * :Eve');
|
|
159
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
164
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
160
165
|
|
|
161
166
|
await client.send('QUIT :bye');
|
|
162
167
|
// The disconnect effect should close the socket.
|
|
@@ -166,18 +171,42 @@ describe('local-cli e2e', () => {
|
|
|
166
171
|
await next.opened();
|
|
167
172
|
await next.send('NICK eve');
|
|
168
173
|
await next.send('USER eve 0 * :Eve2');
|
|
169
|
-
await next.waitFor((l) => l.startsWith(':irc.example.com 001 '), 2000);
|
|
174
|
+
await next.waitFor((l) => l.startsWith(':irc.local.example.com 001 '), 2000);
|
|
170
175
|
await next.close();
|
|
171
176
|
});
|
|
172
177
|
});
|
|
173
178
|
|
|
174
179
|
describe('local-cli error paths', () => {
|
|
175
180
|
it('rejects startLocalServer when the port is already in use', async () => {
|
|
176
|
-
const first = await startLocalServer({
|
|
177
|
-
|
|
181
|
+
const first = await startLocalServer({
|
|
182
|
+
port: 0,
|
|
183
|
+
hostname: '127.0.0.1',
|
|
184
|
+
serverName: 'irc.local.example.com',
|
|
185
|
+
networkName: 'LocalNet',
|
|
186
|
+
});
|
|
187
|
+
await expect(
|
|
188
|
+
startLocalServer({
|
|
189
|
+
port: first.port,
|
|
190
|
+
hostname: '127.0.0.1',
|
|
191
|
+
serverName: 'irc.local.example.com',
|
|
192
|
+
networkName: 'LocalNet',
|
|
193
|
+
}),
|
|
194
|
+
).rejects.toThrow();
|
|
178
195
|
await first.close();
|
|
179
196
|
});
|
|
180
197
|
|
|
198
|
+
it('rejects when serverName is omitted so the placeholder never leaks', async () => {
|
|
199
|
+
await expect(startLocalServer({ port: 0, hostname: '127.0.0.1' })).rejects.toThrowError(
|
|
200
|
+
/serverName/u,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('rejects when networkName is omitted', async () => {
|
|
205
|
+
await expect(
|
|
206
|
+
startLocalServer({ port: 0, hostname: '127.0.0.1', serverName: 'irc.local.example.com' }),
|
|
207
|
+
).rejects.toThrowError(/networkName/u);
|
|
208
|
+
});
|
|
209
|
+
|
|
181
210
|
it('validates the supplied config through the shared ServerConfigSchema at boot', async () => {
|
|
182
211
|
// nickLen=0 is rejected by the schema (must be a positive integer).
|
|
183
212
|
await expect(
|
|
@@ -192,7 +221,12 @@ describe('local-cli error paths', () => {
|
|
|
192
221
|
});
|
|
193
222
|
|
|
194
223
|
it('logs and recovers from a per-socket error without crashing the server', async () => {
|
|
195
|
-
const srv = await startLocalServer({
|
|
224
|
+
const srv = await startLocalServer({
|
|
225
|
+
port: 0,
|
|
226
|
+
hostname: '127.0.0.1',
|
|
227
|
+
serverName: 'irc.local.example.com',
|
|
228
|
+
networkName: 'LocalNet',
|
|
229
|
+
});
|
|
196
230
|
const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
|
|
197
231
|
await client.opened();
|
|
198
232
|
// Synthesize a socket-level error on the SERVER-side ws via the
|
|
@@ -218,20 +252,22 @@ describe('local-cli error paths', () => {
|
|
|
218
252
|
const srv = await startLocalServer({
|
|
219
253
|
port: 0,
|
|
220
254
|
hostname: '127.0.0.1',
|
|
255
|
+
serverName: 'irc.local.example.com',
|
|
256
|
+
networkName: 'LocalNet',
|
|
221
257
|
motdLines: ['first line of motd', 'second line of motd'],
|
|
222
258
|
});
|
|
223
259
|
const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
|
|
224
260
|
await client.opened();
|
|
225
261
|
await client.send('NICK karl');
|
|
226
262
|
await client.send('USER karl 0 * :Karl');
|
|
227
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
263
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
228
264
|
// Drain registration burst so the explicit MOTD responses below aren't
|
|
229
265
|
// confused with the placeholder MOTD emitted at registration.
|
|
230
266
|
client.received.length = 0;
|
|
231
267
|
await client.send('MOTD');
|
|
232
268
|
// Wait for end-of-MOTD so all 372 lines are buffered in `received`.
|
|
233
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 376 karl'));
|
|
234
|
-
const motdBody = client.received.filter((l) => l.startsWith(':irc.example.com 372 karl'));
|
|
269
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 376 karl'));
|
|
270
|
+
const motdBody = client.received.filter((l) => l.startsWith(':irc.local.example.com 372 karl'));
|
|
235
271
|
expect(motdBody.some((l) => l.includes('first line of motd'))).toBe(true);
|
|
236
272
|
expect(motdBody.some((l) => l.includes('second line of motd'))).toBe(true);
|
|
237
273
|
await client.close();
|
|
@@ -239,7 +275,11 @@ describe('local-cli error paths', () => {
|
|
|
239
275
|
});
|
|
240
276
|
|
|
241
277
|
it('defaults hostname to 127.0.0.1 when omitted', async () => {
|
|
242
|
-
const srv = await startLocalServer({
|
|
278
|
+
const srv = await startLocalServer({
|
|
279
|
+
port: 0,
|
|
280
|
+
serverName: 'irc.local.example.com',
|
|
281
|
+
networkName: 'LocalNet',
|
|
282
|
+
});
|
|
243
283
|
expect(srv.hostname).toBe('127.0.0.1');
|
|
244
284
|
// Sanity-check that the server is actually reachable on the default.
|
|
245
285
|
const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
|
|
@@ -249,7 +289,12 @@ describe('local-cli error paths', () => {
|
|
|
249
289
|
});
|
|
250
290
|
|
|
251
291
|
it('close() forcibly closes any still-open client sockets', async () => {
|
|
252
|
-
const srv = await startLocalServer({
|
|
292
|
+
const srv = await startLocalServer({
|
|
293
|
+
port: 0,
|
|
294
|
+
hostname: '127.0.0.1',
|
|
295
|
+
serverName: 'irc.local.example.com',
|
|
296
|
+
networkName: 'LocalNet',
|
|
297
|
+
});
|
|
253
298
|
const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
|
|
254
299
|
await client.opened();
|
|
255
300
|
// Close the server WITHOUT first closing the client — exercises the
|
|
@@ -260,14 +305,24 @@ describe('local-cli error paths', () => {
|
|
|
260
305
|
});
|
|
261
306
|
|
|
262
307
|
it('close() is a no-op when no connections are live', async () => {
|
|
263
|
-
const srv = await startLocalServer({
|
|
308
|
+
const srv = await startLocalServer({
|
|
309
|
+
port: 0,
|
|
310
|
+
hostname: '127.0.0.1',
|
|
311
|
+
serverName: 'irc.local.example.com',
|
|
312
|
+
networkName: 'LocalNet',
|
|
313
|
+
});
|
|
264
314
|
await srv.close();
|
|
265
315
|
// Closing again should not throw (idempotent).
|
|
266
316
|
await expect(srv.close()).resolves.toBeUndefined();
|
|
267
317
|
});
|
|
268
318
|
|
|
269
319
|
it('logs an actor failure without crashing the server (Error path)', async () => {
|
|
270
|
-
const srv = await startLocalServer({
|
|
320
|
+
const srv = await startLocalServer({
|
|
321
|
+
port: 0,
|
|
322
|
+
hostname: '127.0.0.1',
|
|
323
|
+
serverName: 'irc.local.example.com',
|
|
324
|
+
networkName: 'LocalNet',
|
|
325
|
+
});
|
|
271
326
|
// Force the runtime's `send` to throw a real Error so the actor's
|
|
272
327
|
// reducer pipeline rejects with an Error instance — exercises
|
|
273
328
|
// `stringifyErr`'s `instanceof Error` branch and `logError`.
|
|
@@ -291,14 +346,19 @@ describe('local-cli error paths', () => {
|
|
|
291
346
|
await next.opened();
|
|
292
347
|
await next.send('NICK grace');
|
|
293
348
|
await next.send('USER grace 0 * :Grace');
|
|
294
|
-
await next.waitFor((l) => l.startsWith(':irc.example.com 001 '), 2000);
|
|
349
|
+
await next.waitFor((l) => l.startsWith(':irc.local.example.com 001 '), 2000);
|
|
295
350
|
await next.close();
|
|
296
351
|
await client.close();
|
|
297
352
|
await srv.close();
|
|
298
353
|
});
|
|
299
354
|
|
|
300
355
|
it('logs a non-Error throw from the actor pipeline (defensive path)', async () => {
|
|
301
|
-
const srv = await startLocalServer({
|
|
356
|
+
const srv = await startLocalServer({
|
|
357
|
+
port: 0,
|
|
358
|
+
hostname: '127.0.0.1',
|
|
359
|
+
serverName: 'irc.local.example.com',
|
|
360
|
+
networkName: 'LocalNet',
|
|
361
|
+
});
|
|
302
362
|
const original = srv.runtime.send.bind(srv.runtime);
|
|
303
363
|
srv.runtime.send = async () => {
|
|
304
364
|
// Throw a non-Error value to exercise stringifyErr's else branch.
|
|
@@ -316,12 +376,17 @@ describe('local-cli error paths', () => {
|
|
|
316
376
|
});
|
|
317
377
|
|
|
318
378
|
it('cleans up when a client terminates abruptly (server-side error path)', async () => {
|
|
319
|
-
const srv = await startLocalServer({
|
|
379
|
+
const srv = await startLocalServer({
|
|
380
|
+
port: 0,
|
|
381
|
+
hostname: '127.0.0.1',
|
|
382
|
+
serverName: 'irc.local.example.com',
|
|
383
|
+
networkName: 'LocalNet',
|
|
384
|
+
});
|
|
320
385
|
const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
|
|
321
386
|
await client.opened();
|
|
322
387
|
await client.send('NICK ivan');
|
|
323
388
|
await client.send('USER ivan 0 * :Ivan');
|
|
324
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
389
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
325
390
|
const connId = await srv.runtime.lookupNick('ivan');
|
|
326
391
|
expect(connId).not.toBeNull();
|
|
327
392
|
expect(srv.runtime.hasConnection(connId as string)).toBe(true);
|
|
@@ -330,7 +395,7 @@ describe('local-cli error paths', () => {
|
|
|
330
395
|
// 'error' followed by 'close', and the cleanup handler unregisters the
|
|
331
396
|
// connection from the runtime. (The nick itself is released by the
|
|
332
397
|
// QUIT reducer on graceful shutdown; abrupt termination leaks the nick
|
|
333
|
-
// pending a future gone-connection sweep
|
|
398
|
+
// pending a future gone-connection sweep.)
|
|
334
399
|
client.ws.terminate();
|
|
335
400
|
await new Promise((r) => setTimeout(r, 100));
|
|
336
401
|
expect(srv.runtime.hasConnection(connId as string)).toBe(false);
|
|
@@ -338,12 +403,17 @@ describe('local-cli error paths', () => {
|
|
|
338
403
|
});
|
|
339
404
|
|
|
340
405
|
it('delivers an ERROR notice when a reducer supplies a disconnect reason', async () => {
|
|
341
|
-
const srv = await startLocalServer({
|
|
406
|
+
const srv = await startLocalServer({
|
|
407
|
+
port: 0,
|
|
408
|
+
hostname: '127.0.0.1',
|
|
409
|
+
serverName: 'irc.local.example.com',
|
|
410
|
+
networkName: 'LocalNet',
|
|
411
|
+
});
|
|
342
412
|
const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
|
|
343
413
|
await client.opened();
|
|
344
414
|
await client.send('NICK heidi');
|
|
345
415
|
await client.send('USER heidi 0 * :Heidi');
|
|
346
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
416
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
347
417
|
|
|
348
418
|
// Call the runtime's disconnect directly with a reason (simulates a
|
|
349
419
|
// future flood-control / oper-kill effect). The runtime takes a ConnId;
|
|
@@ -370,12 +440,17 @@ describe('local-cli chathistory end-to-end', () => {
|
|
|
370
440
|
client.send(`NICK ${nick}\r\n`);
|
|
371
441
|
client.send(`USER ${nick} 0 * :${nick}\r\n`);
|
|
372
442
|
client.send('CAP END\r\n');
|
|
373
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
443
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
374
444
|
return client;
|
|
375
445
|
}
|
|
376
446
|
|
|
377
447
|
it('records a PRIVMSG and replays it via CHATHISTORY LATEST in a BATCH', async () => {
|
|
378
|
-
const srv = await startLocalServer({
|
|
448
|
+
const srv = await startLocalServer({
|
|
449
|
+
port: 0,
|
|
450
|
+
hostname: '127.0.0.1',
|
|
451
|
+
serverName: 'irc.local.example.com',
|
|
452
|
+
networkName: 'LocalNet',
|
|
453
|
+
});
|
|
379
454
|
const alice = await registerWithCaps(srv, 'ch-alice');
|
|
380
455
|
|
|
381
456
|
alice.send('JOIN #chathist-e2e\r\n');
|
|
@@ -395,7 +470,12 @@ describe('local-cli chathistory end-to-end', () => {
|
|
|
395
470
|
});
|
|
396
471
|
|
|
397
472
|
it('prepends a chathistory BATCH on rejoin after messages were sent', async () => {
|
|
398
|
-
const srv = await startLocalServer({
|
|
473
|
+
const srv = await startLocalServer({
|
|
474
|
+
port: 0,
|
|
475
|
+
hostname: '127.0.0.1',
|
|
476
|
+
serverName: 'irc.local.example.com',
|
|
477
|
+
networkName: 'LocalNet',
|
|
478
|
+
});
|
|
399
479
|
|
|
400
480
|
// Seed history as alice: join + send a message that bob has missed.
|
|
401
481
|
const alice = await registerWithCaps(srv, 'ch-seed');
|
|
@@ -429,6 +509,8 @@ describe('local-cli SASL PLAIN end-to-end', () => {
|
|
|
429
509
|
const srv = await startLocalServer({
|
|
430
510
|
port: 0,
|
|
431
511
|
hostname: '127.0.0.1',
|
|
512
|
+
serverName: 'irc.local.example.com',
|
|
513
|
+
networkName: 'LocalNet',
|
|
432
514
|
saslAccounts: [{ username: 'sasl-alice', password: 's3cret' }],
|
|
433
515
|
});
|
|
434
516
|
|
|
@@ -445,7 +527,7 @@ describe('local-cli SASL PLAIN end-to-end', () => {
|
|
|
445
527
|
client.send('NICK sasl-alice\r\n');
|
|
446
528
|
client.send('USER sasl-alice 0 * :Sasl Alice\r\n');
|
|
447
529
|
client.send('CAP END\r\n');
|
|
448
|
-
await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
|
|
530
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
449
531
|
await client.close();
|
|
450
532
|
await srv.close();
|
|
451
533
|
});
|
|
@@ -454,6 +536,8 @@ describe('local-cli SASL PLAIN end-to-end', () => {
|
|
|
454
536
|
const srv = await startLocalServer({
|
|
455
537
|
port: 0,
|
|
456
538
|
hostname: '127.0.0.1',
|
|
539
|
+
serverName: 'irc.local.example.com',
|
|
540
|
+
networkName: 'LocalNet',
|
|
457
541
|
saslAccounts: [{ username: 'sasl-bob', password: 'right' }],
|
|
458
542
|
});
|
|
459
543
|
|