serverless-ircd 0.3.0 → 0.5.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 +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +349 -18
- package/README.md +132 -30
- package/apps/aws-stack/README.md +6 -5
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +150 -10
- package/apps/aws-stack/tests/stack.test.ts +145 -4
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +149 -24
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +75 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +96 -9
- package/packages/aws-adapter/src/handlers/default.ts +98 -7
- package/packages/aws-adapter/src/handlers/index.ts +109 -4
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +8 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +174 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +302 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +5 -1
- package/packages/cf-adapter/src/cf-runtime.ts +68 -5
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +278 -85
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +54 -11
- package/packages/cf-adapter/src/index.ts +11 -8
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- 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 +12 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
- package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -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 +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
- 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 +15 -8
- 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/wrangler.test.toml +15 -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 +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +24 -3
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +17 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +6 -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 +4 -8
- 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/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- 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/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +65 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +33 -30
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +360 -12
- package/packages/irc-core/src/protocol/numerics.ts +48 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +46 -2
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -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 +57 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- 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 +9 -35
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +102 -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/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- 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-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +393 -16
- package/packages/irc-server/src/dispatch.ts +1 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +15 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +104 -0
- package/packages/irc-server/tests/actor.test.ts +1489 -4
- package/packages/irc-server/tests/dispatch.test.ts +37 -17
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +5 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +141 -3
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +10 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +9 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +107 -0
- 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
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -443
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type ChanName,
|
|
31
31
|
type ChannelState,
|
|
32
32
|
type Clock,
|
|
33
|
+
type ConnSnapshot,
|
|
33
34
|
type ConnectionState,
|
|
34
35
|
Effect,
|
|
35
36
|
type Effect as EffectType,
|
|
@@ -39,23 +40,38 @@ import {
|
|
|
39
40
|
type Logger,
|
|
40
41
|
type MessageStore,
|
|
41
42
|
type MotdProvider,
|
|
43
|
+
type MtlsIdentityProvider,
|
|
44
|
+
type NickHistoryStore,
|
|
42
45
|
NoopLoggerInstance,
|
|
43
46
|
Numerics,
|
|
44
47
|
type RawLine,
|
|
45
48
|
type ServerConfig,
|
|
49
|
+
type ServerStats,
|
|
46
50
|
buildCtx,
|
|
47
51
|
chathistoryReducer,
|
|
48
52
|
handleJoinZero,
|
|
49
53
|
isChannelTarget,
|
|
54
|
+
isonReducer,
|
|
55
|
+
killReducer,
|
|
50
56
|
listReducer,
|
|
57
|
+
lusersReducer,
|
|
51
58
|
parse,
|
|
59
|
+
rehashLine,
|
|
60
|
+
rehashReducer,
|
|
61
|
+
statsReducer,
|
|
62
|
+
toSnapshot,
|
|
63
|
+
traceReducer,
|
|
64
|
+
userhostReducer,
|
|
65
|
+
wallopsReducer,
|
|
52
66
|
whoReducer,
|
|
53
67
|
whoisReducer,
|
|
68
|
+
whowasReducer,
|
|
54
69
|
} from '@serverless-ircd/irc-core';
|
|
55
70
|
import { dispatch } from './dispatch.js';
|
|
56
71
|
import type { DispatchOptions } from './dispatch.js';
|
|
57
72
|
import { type RoutedReducers, buildRoutedReducers } from './routing.js';
|
|
58
73
|
import type { IrcRuntime } from './runtime.js';
|
|
74
|
+
import { type Transport, WsTextFrameTransport } from './transport.js';
|
|
59
75
|
|
|
60
76
|
/**
|
|
61
77
|
* Channel-state access for the actor.
|
|
@@ -92,6 +108,13 @@ export interface ConnectionActorOptions {
|
|
|
92
108
|
runtime: IrcRuntime;
|
|
93
109
|
channels: ActorChannelAccess;
|
|
94
110
|
serverConfig: ServerConfig;
|
|
111
|
+
/**
|
|
112
|
+
* Short label identifying the adapter's bound config source (e.g.
|
|
113
|
+
* `"KV"`, `"Secrets Manager"`, `"config file"`). Surfaced in the
|
|
114
|
+
* `382 RPL_REHASHING` reply's trailing text so the oper can see where the
|
|
115
|
+
* reload was attempted from. Defaults to `"server config"` when omitted.
|
|
116
|
+
*/
|
|
117
|
+
readonly configSource?: string;
|
|
95
118
|
clock: Clock;
|
|
96
119
|
ids: IdFactory;
|
|
97
120
|
motd?: MotdProvider;
|
|
@@ -114,6 +137,33 @@ export interface ConnectionActorOptions {
|
|
|
114
137
|
* can vouch for the credentials.
|
|
115
138
|
*/
|
|
116
139
|
readonly accounts?: AccountStore;
|
|
140
|
+
/**
|
|
141
|
+
* mTLS client-cert identity source for SASL EXTERNAL (absent when mTLS
|
|
142
|
+
* is not configured). When bound, `ctx.mtlsIdentity` is defined so the
|
|
143
|
+
* `AUTHENTICATE EXTERNAL` reducer can resolve the connection's verified
|
|
144
|
+
* certificate subject. Omitted (→ `ctx.mtlsIdentity` undefined) means
|
|
145
|
+
* EXTERNAL is rejected with `904 ERR_SASLFAIL`.
|
|
146
|
+
*/
|
|
147
|
+
readonly mtlsIdentity?: MtlsIdentityProvider;
|
|
148
|
+
/**
|
|
149
|
+
* Nick-history source for the `WHOWAS` command (absent when WHOWAS
|
|
150
|
+
* history retention is disabled). When bound, `ctx.history` is defined
|
|
151
|
+
* so the QUIT and NICK-change reducers record sign-off entries and the
|
|
152
|
+
* WHOWAS reducer replays them. Omitted (→ `ctx.history` undefined)
|
|
153
|
+
* preserves the no-store behaviour: `WHOWAS <nick>` emits `406`+`369`.
|
|
154
|
+
*/
|
|
155
|
+
readonly history?: NickHistoryStore;
|
|
156
|
+
/**
|
|
157
|
+
* Network-statistics source for the `LUSERS` and `STATS` commands
|
|
158
|
+
* (absent when no stats backend is wired). When bound, the actor
|
|
159
|
+
* awaits `stats.getStats()` once per LUSERS/STATS invocation and hands
|
|
160
|
+
* the resulting snapshot to the pure reducers — same pattern as
|
|
161
|
+
* `listChannels` → `listReducer`. Omitted (→ a zero-count fallback)
|
|
162
|
+
* preserves graceful behaviour for deployments/tests that have not
|
|
163
|
+
* opted in: `LUSERS` reports zero users/channels, `STATS u` reports
|
|
164
|
+
* zero uptime.
|
|
165
|
+
*/
|
|
166
|
+
readonly stats?: ServerStats;
|
|
117
167
|
/**
|
|
118
168
|
* Structured-logger port. When omitted the actor runs
|
|
119
169
|
* silently — observability is purely opt-in. When supplied, every
|
|
@@ -127,19 +177,39 @@ export interface ConnectionActorOptions {
|
|
|
127
177
|
* still uniquely identifiable in logs.
|
|
128
178
|
*/
|
|
129
179
|
readonly traceId?: string;
|
|
180
|
+
/**
|
|
181
|
+
* Line-framing seam (PLAN §9, ADR-009). Turns inbound transport input
|
|
182
|
+
* into complete IRC lines. Defaults to a {@link WsTextFrameTransport}
|
|
183
|
+
* (one message per WebSocket text frame, the serverless default). A raw
|
|
184
|
+
* TCP+TLS adapter injects a {@link TcpByteStreamTransport} for stateful
|
|
185
|
+
* `\r\n` byte-stream framing across chunks. The parser/reducer/dispatch
|
|
186
|
+
* pipeline is shared unchanged across both transports.
|
|
187
|
+
*/
|
|
188
|
+
readonly transport?: Transport;
|
|
130
189
|
}
|
|
131
190
|
|
|
132
191
|
export class ConnectionActor {
|
|
133
192
|
private readonly state: ConnectionState;
|
|
134
193
|
private readonly runtime: IrcRuntime;
|
|
135
194
|
private readonly channels: ActorChannelAccess;
|
|
136
|
-
private
|
|
195
|
+
private serverConfig: ServerConfig;
|
|
196
|
+
private readonly configSource: string;
|
|
137
197
|
private readonly clock: Clock;
|
|
138
198
|
private readonly ids: IdFactory;
|
|
139
199
|
private readonly motd: MotdProvider;
|
|
140
200
|
private readonly messages: MessageStore | undefined;
|
|
141
201
|
private readonly accounts: AccountStore | undefined;
|
|
202
|
+
private readonly mtlsIdentity: MtlsIdentityProvider | undefined;
|
|
203
|
+
private readonly history: NickHistoryStore | undefined;
|
|
204
|
+
private readonly stats: ServerStats | undefined;
|
|
142
205
|
private readonly reducers: RoutedReducers;
|
|
206
|
+
/**
|
|
207
|
+
* Line-framing seam. Turns inbound input (a WS text frame or a raw TCP
|
|
208
|
+
* chunk) into complete IRC lines. Defaulted to a WS-frame transport for
|
|
209
|
+
* the serverless default; a TCP+TLS adapter injects the stateful
|
|
210
|
+
* byte-stream transport at construction.
|
|
211
|
+
*/
|
|
212
|
+
private readonly transport: Transport;
|
|
143
213
|
/**
|
|
144
214
|
* Bound logger. Defaults to a no-op so the actor pipeline has no
|
|
145
215
|
* observable side effects when callers do not opt in.
|
|
@@ -158,11 +228,15 @@ export class ConnectionActor {
|
|
|
158
228
|
this.runtime = opts.runtime;
|
|
159
229
|
this.channels = opts.channels;
|
|
160
230
|
this.serverConfig = opts.serverConfig;
|
|
231
|
+
this.configSource = opts.configSource ?? 'server config';
|
|
161
232
|
this.clock = opts.clock;
|
|
162
233
|
this.ids = opts.ids;
|
|
163
234
|
this.motd = opts.motd ?? EmptyMotdProvider;
|
|
164
235
|
this.messages = opts.messages;
|
|
165
236
|
this.accounts = opts.accounts;
|
|
237
|
+
this.mtlsIdentity = opts.mtlsIdentity;
|
|
238
|
+
this.history = opts.history;
|
|
239
|
+
this.stats = opts.stats;
|
|
166
240
|
this.logger = opts.logger ?? NoopLoggerInstance;
|
|
167
241
|
this.pinnedTraceId = opts.traceId;
|
|
168
242
|
// Bind the connection id once so every record the actor emits carries
|
|
@@ -176,15 +250,19 @@ export class ConnectionActor {
|
|
|
176
250
|
// reducer when flood control is disabled) so a flooding client is bounded
|
|
177
251
|
// regardless of which authority owns the command's state.
|
|
178
252
|
this.reducers = buildRoutedReducers(opts.serverConfig);
|
|
253
|
+
this.transport = opts.transport ?? new WsTextFrameTransport();
|
|
179
254
|
}
|
|
180
255
|
|
|
181
256
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
257
|
+
* Transport-agnostic entry point: feeds `chunk` through the bound
|
|
258
|
+
* {@link Transport} and dispatches each complete, non-empty line in order.
|
|
259
|
+
* A WS adapter passes one text frame per call; a raw TCP+TLS adapter
|
|
260
|
+
* passes one arbitrary byte chunk per call and the transport buffers any
|
|
261
|
+
* partial trailing line across calls. A parse failure on one line emits a
|
|
262
|
+
* `421` and processing continues with the next line.
|
|
185
263
|
*/
|
|
186
|
-
async
|
|
187
|
-
const lines =
|
|
264
|
+
async receive(chunk: string): Promise<void> {
|
|
265
|
+
const lines = this.transport.feed(chunk);
|
|
188
266
|
if (lines.length === 0) return;
|
|
189
267
|
const traceId = this.pinnedTraceId ?? this.ids.traceId();
|
|
190
268
|
const log = this.logger.child({ traceId });
|
|
@@ -194,6 +272,17 @@ export class ConnectionActor {
|
|
|
194
272
|
}
|
|
195
273
|
}
|
|
196
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Processes one WebSocket text frame. Equivalent to {@link receive} — kept
|
|
277
|
+
* as the named entry point every WS adapter already calls. Splits on
|
|
278
|
+
* `\r\n` (tolerating bare `\n`) via the default WS-frame transport and
|
|
279
|
+
* dispatches each non-empty line in order. A parse failure on one line
|
|
280
|
+
* emits a `421` and processing continues with the next line.
|
|
281
|
+
*/
|
|
282
|
+
async receiveTextFrame(text: string): Promise<void> {
|
|
283
|
+
await this.receive(text);
|
|
284
|
+
}
|
|
285
|
+
|
|
197
286
|
private async processLine(line: string, log: Logger, traceId: string): Promise<void> {
|
|
198
287
|
let msg: IrcMessage;
|
|
199
288
|
let parseFailed = false;
|
|
@@ -256,6 +345,8 @@ export class ConnectionActor {
|
|
|
256
345
|
connection: this.state,
|
|
257
346
|
...(this.messages !== undefined ? { messages: this.messages } : {}),
|
|
258
347
|
...(this.accounts !== undefined ? { accounts: this.accounts } : {}),
|
|
348
|
+
...(this.mtlsIdentity !== undefined ? { mtlsIdentity: this.mtlsIdentity } : {}),
|
|
349
|
+
...(this.history !== undefined ? { history: this.history } : {}),
|
|
259
350
|
});
|
|
260
351
|
|
|
261
352
|
switch (msg.command) {
|
|
@@ -276,12 +367,30 @@ export class ConnectionActor {
|
|
|
276
367
|
return this.reducers.motd(this.state, msg, ctx).effects;
|
|
277
368
|
case 'OPER':
|
|
278
369
|
return this.reducers.oper(this.state, msg, ctx).effects;
|
|
370
|
+
case 'REHASH':
|
|
371
|
+
return this.routeRehash(msg, ctx);
|
|
279
372
|
case 'CAP':
|
|
280
373
|
return this.reducers.cap(this.state, msg, ctx).effects;
|
|
281
374
|
case 'AUTHENTICATE':
|
|
282
375
|
return this.reducers.authenticate(this.state, msg, ctx).effects;
|
|
283
376
|
case 'AWAY':
|
|
284
377
|
return this.reducers.away(this.state, msg, ctx).effects;
|
|
378
|
+
case 'SETNAME':
|
|
379
|
+
return this.reducers.setname(this.state, msg, ctx).effects;
|
|
380
|
+
|
|
381
|
+
// -------- Server-info query commands --------
|
|
382
|
+
case 'VERSION':
|
|
383
|
+
return this.reducers.version(this.state, msg, ctx).effects;
|
|
384
|
+
case 'TIME':
|
|
385
|
+
return this.reducers.time(this.state, msg, ctx).effects;
|
|
386
|
+
case 'ADMIN':
|
|
387
|
+
return this.reducers.admin(this.state, msg, ctx).effects;
|
|
388
|
+
case 'INFO':
|
|
389
|
+
return this.reducers.info(this.state, msg, ctx).effects;
|
|
390
|
+
case 'USERHOST':
|
|
391
|
+
return this.routeUserhost(msg, ctx);
|
|
392
|
+
case 'ISON':
|
|
393
|
+
return this.routeIson(msg, ctx);
|
|
285
394
|
|
|
286
395
|
// -------- Channel-authority commands --------
|
|
287
396
|
case 'JOIN':
|
|
@@ -305,6 +414,18 @@ export class ConnectionActor {
|
|
|
305
414
|
return this.routeWho(msg, ctx);
|
|
306
415
|
case 'WHOIS':
|
|
307
416
|
return this.routeWhois(msg, ctx);
|
|
417
|
+
case 'WHOWAS':
|
|
418
|
+
return this.routeWhowas(msg, ctx);
|
|
419
|
+
case 'KILL':
|
|
420
|
+
return this.routeKill(msg, ctx);
|
|
421
|
+
case 'TRACE':
|
|
422
|
+
return this.routeTrace(msg, ctx);
|
|
423
|
+
case 'WALLOPS':
|
|
424
|
+
return this.routeWallops(msg, ctx);
|
|
425
|
+
case 'LUSERS':
|
|
426
|
+
return this.routeLusers(msg, ctx);
|
|
427
|
+
case 'STATS':
|
|
428
|
+
return this.routeStats(msg, ctx);
|
|
308
429
|
case 'CHATHISTORY':
|
|
309
430
|
return this.routeChathistory(msg, ctx);
|
|
310
431
|
|
|
@@ -339,6 +460,19 @@ export class ConnectionActor {
|
|
|
339
460
|
: this.reducers.userMode(this.state, msg, ctx).effects;
|
|
340
461
|
|
|
341
462
|
default:
|
|
463
|
+
// Remaining standard IRC verbs not yet implemented. They fall through
|
|
464
|
+
// to a graceful `421 ERR_UNKNOWNCOMMAND`. Triage of remaining verbs:
|
|
465
|
+
// - Deferred (serverless-incompatible / needs new infra):
|
|
466
|
+
// (none — LUSERS/STATS are now implemented).
|
|
467
|
+
// - Deliberately dropped (S2S — PLAN non-goal):
|
|
468
|
+
// CONNECT, SQUIT, LINKS. There is no server mesh to connect,
|
|
469
|
+
// split, or list; these stay 421. Their reserved numerics
|
|
470
|
+
// (RPL_LINKS/RPL_ENDOFLINKS/ERR_CANTKILLSERVER) were removed.
|
|
471
|
+
// - Deliberately dropped (RFC 2812 obsolete, never widely
|
|
472
|
+
// implemented): SERVICE, SUMMON, USERS. These
|
|
473
|
+
// stay 421; their reserved numerics
|
|
474
|
+
// (RPL_YOURESERVICE/ERR_NOSUCHSERVICE/ERR_SUMMONDISABLED/
|
|
475
|
+
// ERR_USERSDISABLED) were removed.
|
|
342
476
|
return [this.unknownCommandEffect(msg.command)];
|
|
343
477
|
}
|
|
344
478
|
}
|
|
@@ -351,6 +485,40 @@ export class ConnectionActor {
|
|
|
351
485
|
return listReducer(snapshots, msg, ctx).effects;
|
|
352
486
|
}
|
|
353
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Routes `REHASH` (oper-gated config reload). The pure reducer enforces the
|
|
490
|
+
* oper gate and emits `382 RPL_REHASHING`; when the gate passes the actor
|
|
491
|
+
* re-invokes the runtime's bound config loader and swaps its live
|
|
492
|
+
* `serverConfig` reference so subsequent commands observe the refreshed
|
|
493
|
+
* value (rotated oper creds, new MOTD source, etc.).
|
|
494
|
+
*
|
|
495
|
+
* On reload failure the previous config stays in effect and a graceful
|
|
496
|
+
* error-suffixed `382` follows — the oper is NOT disconnected (per the
|
|
497
|
+
* serverless reload contract: a bad store read must never sever a live
|
|
498
|
+
* connection).
|
|
499
|
+
*/
|
|
500
|
+
private async routeRehash(
|
|
501
|
+
msg: IrcMessage,
|
|
502
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
503
|
+
): Promise<EffectType[]> {
|
|
504
|
+
const result = rehashReducer(this.state, msg, ctx, this.configSource);
|
|
505
|
+
if (!result.shouldReload) {
|
|
506
|
+
return result.effects;
|
|
507
|
+
}
|
|
508
|
+
const effects = [...result.effects];
|
|
509
|
+
try {
|
|
510
|
+
this.serverConfig = await this.runtime.reloadConfig();
|
|
511
|
+
} catch (err) {
|
|
512
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
513
|
+
this.logger.warn('rehash.reload-failed', { reason });
|
|
514
|
+
const nick = this.state.nick ?? '*';
|
|
515
|
+
effects.push(
|
|
516
|
+
Effect.send(this.state.id, [rehashLine(ctx.serverName, nick, this.configSource, reason)]),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return effects;
|
|
520
|
+
}
|
|
521
|
+
|
|
354
522
|
private async routeWho(msg: IrcMessage, ctx: ReturnType<typeof buildCtx>): Promise<EffectType[]> {
|
|
355
523
|
const target = msg.params[0] ?? '';
|
|
356
524
|
if (target.length === 0) {
|
|
@@ -387,6 +555,207 @@ export class ConnectionActor {
|
|
|
387
555
|
return whoisReducer(target, channels, msg, ctx).effects;
|
|
388
556
|
}
|
|
389
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Routes `KILL <nick> <comment>` to the kill reducer. The actor resolves
|
|
560
|
+
* the target via the nick registry + a live connection fetch (mirroring
|
|
561
|
+
* {@link routeWhois}); no channel is created as a side effect. The
|
|
562
|
+
* reducer performs the oper gate and, on success, emits a
|
|
563
|
+
* {@link DisconnectEffect} for the target — the runtime's `disconnect`
|
|
564
|
+
* closes the target transport and fans QUIT out to shared-channel peers
|
|
565
|
+
* (peer fanout is the runtime's responsibility, not the reducer's).
|
|
566
|
+
*/
|
|
567
|
+
private async routeKill(
|
|
568
|
+
msg: IrcMessage,
|
|
569
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
570
|
+
): Promise<EffectType[]> {
|
|
571
|
+
const targetNick = msg.params[0] ?? '';
|
|
572
|
+
let target: ConnectionState | undefined;
|
|
573
|
+
if (targetNick.length > 0) {
|
|
574
|
+
const connId = await this.runtime.lookupNick(targetNick);
|
|
575
|
+
if (connId !== null) {
|
|
576
|
+
target = (await this.runtime.getConnection(connId)) ?? undefined;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return killReducer(target, msg, ctx).effects;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Routes `TRACE [<target>]` to the trace reducer. Single-server
|
|
584
|
+
* deployment: when `<target>` is absent or equals the local server name
|
|
585
|
+
* the reducer emits only the `262 RPL_ENDOFTRACE` terminator; when
|
|
586
|
+
* `<target>` resolves to a nick the reducer emits `205`/`204` per-conn
|
|
587
|
+
* detail (oper requester only) plus `262`; any other `<target>` is
|
|
588
|
+
* treated as a non-local server and yields `402 ERR_NOSUCHSERVER`.
|
|
589
|
+
*
|
|
590
|
+
* The actor resolves the target via `lookupNick` + `getConnection`
|
|
591
|
+
* (mirroring {@link routeWhois}). The local-server check is
|
|
592
|
+
* case-insensitive against `serverConfig.serverName`. Multi-hop S2S
|
|
593
|
+
* tracing is a PLAN non-goal.
|
|
594
|
+
*/
|
|
595
|
+
private async routeTrace(
|
|
596
|
+
msg: IrcMessage,
|
|
597
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
598
|
+
): Promise<EffectType[]> {
|
|
599
|
+
const rawTarget = msg.params[0] ?? '';
|
|
600
|
+
if (rawTarget.length === 0) {
|
|
601
|
+
return traceReducer(undefined, msg, ctx).effects;
|
|
602
|
+
}
|
|
603
|
+
// Local server name (case-insensitive) → collapse to the no-target path.
|
|
604
|
+
if (rawTarget.toLowerCase() === ctx.serverName.toLowerCase()) {
|
|
605
|
+
return traceReducer(undefined, msg, ctx).effects;
|
|
606
|
+
}
|
|
607
|
+
// Try to resolve as a nick.
|
|
608
|
+
const connId = await this.runtime.lookupNick(rawTarget);
|
|
609
|
+
if (connId !== null) {
|
|
610
|
+
const target = (await this.runtime.getConnection(connId)) ?? undefined;
|
|
611
|
+
return traceReducer(target, msg, ctx).effects;
|
|
612
|
+
}
|
|
613
|
+
// Unresolved: treat as a remote server → 402 ERR_NOSUCHSERVER.
|
|
614
|
+
return traceReducer(undefined, msg, ctx, rawTarget).effects;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Routes `WALLOPS :<message>` to the wallops reducer. The pure reducer
|
|
619
|
+
* enforces the oper gate (`481`) and the non-empty-message check
|
|
620
|
+
* (`412`); on success it emits a single {@link BroadcastWallopsEffect}
|
|
621
|
+
* that the dispatch layer interprets via the runtime's
|
|
622
|
+
* `broadcastWallops` method — a global fan-out to every `+w`
|
|
623
|
+
* connection (not channel-scoped). The originator is always excluded
|
|
624
|
+
* (skip-self, matching NOTICE/PRIVMSG broadcast semantics).
|
|
625
|
+
*/
|
|
626
|
+
private routeWallops(msg: IrcMessage, ctx: ReturnType<typeof buildCtx>): EffectType[] {
|
|
627
|
+
return wallopsReducer(this.state, msg, ctx).effects;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Routes `WHOWAS <nick>{,<nick>} [<count>]` to the whowas reducer. The
|
|
632
|
+
* command is query-only: it reads the bound nick-history store
|
|
633
|
+
* (`this.history`, absent when no store is wired) and emits
|
|
634
|
+
* `314`/`369`/`406` numerics. No runtime lookups are needed — the store
|
|
635
|
+
* is synchronous and self-contained.
|
|
636
|
+
*/
|
|
637
|
+
private async routeWhowas(
|
|
638
|
+
msg: IrcMessage,
|
|
639
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
640
|
+
): Promise<EffectType[]> {
|
|
641
|
+
return whowasReducer(this.history, msg, ctx).effects;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Routes `LUSERS [<mask> [<server>]]` to the lusers reducer. Awaits the
|
|
646
|
+
* bound {@link ServerStats} backend (or falls back to a zero-count
|
|
647
|
+
* snapshot when no backend is wired) and hands the result to the pure
|
|
648
|
+
* reducer, which emits the `251`–`255`/`265`/`266` numerics.
|
|
649
|
+
*
|
|
650
|
+
* Single-server deployment: a `<server>` argument that does not match
|
|
651
|
+
* the local server name (case-insensitive) is treated as a remote
|
|
652
|
+
* server and yields `402 ERR_NOSUCHSERVER`. S2S aggregation is a PLAN
|
|
653
|
+
* non-goal.
|
|
654
|
+
*/
|
|
655
|
+
private async routeLusers(
|
|
656
|
+
msg: IrcMessage,
|
|
657
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
658
|
+
): Promise<EffectType[]> {
|
|
659
|
+
const serverTarget = serverTargetOf(msg.params[1] ?? '', ctx.serverName);
|
|
660
|
+
const snapshot = await this.fetchStats();
|
|
661
|
+
return lusersReducer(snapshot, msg, ctx, serverTarget).effects;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Routes `STATS <query> [<server>]` to the stats reducer. Awaits the
|
|
666
|
+
* bound {@link ServerStats} backend (or zero-count fallback) and hands
|
|
667
|
+
* the result to the pure reducer. The reducer handles the implemented
|
|
668
|
+
* query letters (`u`, `l`), the unknown-letter charybdis-style empty
|
|
669
|
+
* body, and the `219` terminator; non-local `<server>` arguments
|
|
670
|
+
* produce `402 ERR_NOSUCHSERVER`.
|
|
671
|
+
*/
|
|
672
|
+
private async routeStats(
|
|
673
|
+
msg: IrcMessage,
|
|
674
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
675
|
+
): Promise<EffectType[]> {
|
|
676
|
+
const serverTarget = serverTargetOf(msg.params[1] ?? '', ctx.serverName);
|
|
677
|
+
const snapshot = await this.fetchStats();
|
|
678
|
+
return statsReducer(snapshot, msg, ctx, serverTarget).effects;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Fetches the current {@link ServerStatsSnapshot} from the bound backend.
|
|
683
|
+
* When no backend is wired, returns a zero-count snapshot so LUSERS/STATS
|
|
684
|
+
* still emit a well-formed reply rather than a 421/empty stream — the
|
|
685
|
+
* uptime anchor defaults to the actor's wall clock so `STATS u` reports
|
|
686
|
+
* zero uptime instead of a negative duration.
|
|
687
|
+
*/
|
|
688
|
+
private async fetchStats(): Promise<import('@serverless-ircd/irc-core').ServerStatsSnapshot> {
|
|
689
|
+
if (this.stats !== undefined) {
|
|
690
|
+
return this.stats.getStats();
|
|
691
|
+
}
|
|
692
|
+
const now = this.clock.now();
|
|
693
|
+
return {
|
|
694
|
+
users: 0,
|
|
695
|
+
invisible: 0,
|
|
696
|
+
opers: 0,
|
|
697
|
+
unknownConnections: 0,
|
|
698
|
+
channels: 0,
|
|
699
|
+
servers: 1,
|
|
700
|
+
localConns: 0,
|
|
701
|
+
globalConns: 0,
|
|
702
|
+
maxLocalConns: 0,
|
|
703
|
+
maxGlobalConns: 0,
|
|
704
|
+
uptimeStartedAt: now,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Routes `USERHOST <nick>{ <nick>}` to the userhost reducer. Resolves each
|
|
710
|
+
* requested nick via the nick registry + connection lookup, passing the
|
|
711
|
+
* resulting snapshots (or `null` for offline nicks) to the pure formatter.
|
|
712
|
+
* The number of queried nicks is capped at
|
|
713
|
+
* `serverConfig.maxTargetsPerCommand` by the reducer.
|
|
714
|
+
*/
|
|
715
|
+
private async routeUserhost(
|
|
716
|
+
msg: IrcMessage,
|
|
717
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
718
|
+
): Promise<EffectType[]> {
|
|
719
|
+
const cap = ctx.serverConfig.maxTargetsPerCommand;
|
|
720
|
+
const resolved = new Map<string, ConnSnapshot | null>();
|
|
721
|
+
for (const nick of msg.params.slice(0, cap)) {
|
|
722
|
+
const connId = await this.runtime.lookupNick(nick);
|
|
723
|
+
if (connId === null) {
|
|
724
|
+
resolved.set(nick, null);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
const conn = await this.runtime.getConnection(connId);
|
|
728
|
+
resolved.set(nick, conn === null ? null : toSnapshot(conn));
|
|
729
|
+
}
|
|
730
|
+
return userhostReducer(resolved, msg, ctx).effects;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Routes `ISON <nick>{ <nick>}` to the ison reducer. Resolves each requested
|
|
735
|
+
* nick via the nick registry; for online nicks, fetches the connection to
|
|
736
|
+
* recover the registered (display) spelling. Passes a folded→display map to
|
|
737
|
+
* the pure formatter.
|
|
738
|
+
*/
|
|
739
|
+
private async routeIson(
|
|
740
|
+
msg: IrcMessage,
|
|
741
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
742
|
+
): Promise<EffectType[]> {
|
|
743
|
+
const cap = ctx.serverConfig.maxTargetsPerCommand;
|
|
744
|
+
const online = new Map<string, string>();
|
|
745
|
+
for (const nick of msg.params.slice(0, cap)) {
|
|
746
|
+
const connId = await this.runtime.lookupNick(nick);
|
|
747
|
+
if (connId === null) continue;
|
|
748
|
+
const conn = await this.runtime.getConnection(connId);
|
|
749
|
+
// `conn` can be null in the race where lookupNick resolved but the
|
|
750
|
+
// connection has since torn down (the nick map lags the connection
|
|
751
|
+
// map). Skip those — they are no longer online.
|
|
752
|
+
if (conn?.nick !== undefined) {
|
|
753
|
+
online.set(conn.nick.toLowerCase(), conn.nick);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return isonReducer(online, msg, ctx).effects;
|
|
757
|
+
}
|
|
758
|
+
|
|
390
759
|
/**
|
|
391
760
|
* Routes `CHATHISTORY <sub> <target> [<args>...]` to the chathistory
|
|
392
761
|
* reducer. The command is query-only and must NOT create a channel as a
|
|
@@ -531,15 +900,6 @@ export class ConnectionActor {
|
|
|
531
900
|
}
|
|
532
901
|
}
|
|
533
902
|
|
|
534
|
-
/**
|
|
535
|
-
* Splits a WebSocket text frame into IRC lines. Tolerates `\r\n`, bare `\n`,
|
|
536
|
-
* and missing trailing terminators. Returns each line bare (no CRLF).
|
|
537
|
-
*/
|
|
538
|
-
export function splitFrameLines(text: string): string[] {
|
|
539
|
-
if (text.length === 0) return [];
|
|
540
|
-
return text.split(/\r?\n/u);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
903
|
/** Returns the first whitespace-delimited token of `line`, or `''`. */
|
|
544
904
|
function firstToken(line: string): string {
|
|
545
905
|
const m = line.match(/^\S+/u);
|
|
@@ -555,6 +915,20 @@ function splitCsv(param: string): string[] {
|
|
|
555
915
|
return param.split(',').filter((p) => p.length > 0);
|
|
556
916
|
}
|
|
557
917
|
|
|
918
|
+
/**
|
|
919
|
+
* Resolves the `<server>` parameter shared by `LUSERS` / `STATS` (and any
|
|
920
|
+
* future single-server-aware query verb). Returns `undefined` when the
|
|
921
|
+
* argument is absent OR names the local server (case-insensitive) — the
|
|
922
|
+
* caller then proceeds with the local-server code path. Returns the raw
|
|
923
|
+
* argument verbatim when it names a non-local server, which the reducer
|
|
924
|
+
* surfaces as `402 ERR_NOSUCHSERVER`.
|
|
925
|
+
*/
|
|
926
|
+
function serverTargetOf(rawServer: string, localServerName: string): string | undefined {
|
|
927
|
+
if (rawServer.length === 0) return undefined;
|
|
928
|
+
if (rawServer.toLowerCase() === localServerName.toLowerCase()) return undefined;
|
|
929
|
+
return rawServer;
|
|
930
|
+
}
|
|
931
|
+
|
|
558
932
|
/**
|
|
559
933
|
* Extracts the channel names a reducer will read for the supplied command.
|
|
560
934
|
* Used by `prefetchChannelState` to refresh only the channels the upcoming
|
|
@@ -564,8 +938,11 @@ function splitCsv(param: string): string[] {
|
|
|
564
938
|
* Commands not listed here either have no channel target (PING, NICK, …)
|
|
565
939
|
* or read channel membership from `ctx.connection.joinedChannels` directly
|
|
566
940
|
* (QUIT), so they don't need a prefetch.
|
|
941
|
+
*
|
|
942
|
+
* Exported so the per-command dispatch table can be unit-tested without
|
|
943
|
+
* standing up a full {@link ConnectionActor} + runtime.
|
|
567
944
|
*/
|
|
568
|
-
function channelTargetsForMessage(msg: IrcMessage): ChanName[] {
|
|
945
|
+
export function channelTargetsForMessage(msg: IrcMessage): ChanName[] {
|
|
569
946
|
const cmd = msg.command;
|
|
570
947
|
switch (cmd) {
|
|
571
948
|
case 'JOIN':
|
|
@@ -33,9 +33,7 @@ const handlers: { [K in Effect['tag']]: Handler<Extract<Effect, { tag: K }>> } =
|
|
|
33
33
|
ChangeNick: (e, r) => r.changeNick(e.conn, e.oldNick, e.newNick).then(noop),
|
|
34
34
|
ReleaseNick: (e, r) => r.releaseNick(e.nick),
|
|
35
35
|
ApplyChannelDelta: (e, r) => r.applyChannelDelta(e.chan, e.delta),
|
|
36
|
-
|
|
37
|
-
GetConnectionInfo: (e, r) => r.getConnectionInfo(e.conn).then(noop),
|
|
38
|
-
GetChannelSnapshot: (e, r) => r.getChannelSnapshot(e.chan).then(noop),
|
|
36
|
+
BroadcastWallops: (e, r) => r.broadcastWallops(e.lines, e.except),
|
|
39
37
|
};
|
|
40
38
|
|
|
41
39
|
function noop(): void {}
|
|
@@ -3,11 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Orchestration layer: defines the {@link IrcRuntime} port, the {@link dispatch}
|
|
5
5
|
* interpreter that runs an `Effect[]` against it, and the {@link ConnectionActor}
|
|
6
|
-
* that turns a WebSocket text frame
|
|
6
|
+
* that turns inbound transport input (a WebSocket text frame or a raw TCP byte
|
|
7
|
+
* chunk) into reducer invocations + dispatch via the transport-agnostic
|
|
8
|
+
* {@link Transport} framing seam.
|
|
7
9
|
*/
|
|
8
10
|
export type { IrcRuntime } from './runtime.js';
|
|
9
11
|
export { dispatch } from './dispatch.js';
|
|
10
|
-
export { ConnectionActor
|
|
12
|
+
export { ConnectionActor } from './actor.js';
|
|
11
13
|
export type { ActorChannelAccess, ConnectionActorOptions } from './actor.js';
|
|
14
|
+
export {
|
|
15
|
+
splitFrameLines,
|
|
16
|
+
WsTextFrameTransport,
|
|
17
|
+
TcpByteStreamTransport,
|
|
18
|
+
} from './transport.js';
|
|
19
|
+
export type { Transport } from './transport.js';
|
|
12
20
|
export { buildRoutedReducers, resolveFloodControl } from './routing.js';
|
|
13
21
|
export type { RoutedReducers } from './routing.js';
|
|
@@ -29,11 +29,13 @@ import {
|
|
|
29
29
|
type FloodControlConfig,
|
|
30
30
|
type Reducer,
|
|
31
31
|
type ServerConfig,
|
|
32
|
+
adminReducer,
|
|
32
33
|
authenticateReducer,
|
|
33
34
|
awayReducer,
|
|
34
35
|
capReducer,
|
|
35
36
|
channelModeReducer,
|
|
36
37
|
defaultCommandCost,
|
|
38
|
+
infoReducer,
|
|
37
39
|
inviteReducer,
|
|
38
40
|
joinReducer,
|
|
39
41
|
kickReducer,
|
|
@@ -50,11 +52,14 @@ import {
|
|
|
50
52
|
privmsgChannelReducer,
|
|
51
53
|
privmsgUserReducer,
|
|
52
54
|
quitReducer,
|
|
55
|
+
setnameReducer,
|
|
53
56
|
tagmsgChannelReducer,
|
|
54
57
|
tagmsgUserReducer,
|
|
58
|
+
timeReducer,
|
|
55
59
|
topicReducer,
|
|
56
60
|
userModeReducer,
|
|
57
61
|
userReducer,
|
|
62
|
+
versionReducer,
|
|
58
63
|
wrapWithFloodControl,
|
|
59
64
|
} from '@serverless-ircd/irc-core';
|
|
60
65
|
|
|
@@ -76,6 +81,11 @@ export interface RoutedReducers {
|
|
|
76
81
|
readonly cap: Reducer<ConnectionState>;
|
|
77
82
|
readonly authenticate: Reducer<ConnectionState>;
|
|
78
83
|
readonly away: Reducer<ConnectionState>;
|
|
84
|
+
readonly setname: Reducer<ConnectionState>;
|
|
85
|
+
readonly version: Reducer<ConnectionState>;
|
|
86
|
+
readonly time: Reducer<ConnectionState>;
|
|
87
|
+
readonly admin: Reducer<ConnectionState>;
|
|
88
|
+
readonly info: Reducer<ConnectionState>;
|
|
79
89
|
// Channel-authority reducers.
|
|
80
90
|
readonly join: Reducer<ChannelState>;
|
|
81
91
|
readonly part: Reducer<ChannelState>;
|
|
@@ -142,6 +152,11 @@ export function buildRoutedReducers(serverConfig: ServerConfig): RoutedReducers
|
|
|
142
152
|
cap: wrap(capReducer),
|
|
143
153
|
authenticate: wrap(authenticateReducer),
|
|
144
154
|
away: wrap(awayReducer),
|
|
155
|
+
setname: wrap(setnameReducer),
|
|
156
|
+
version: wrap(versionReducer),
|
|
157
|
+
time: wrap(timeReducer),
|
|
158
|
+
admin: wrap(adminReducer),
|
|
159
|
+
info: wrap(infoReducer),
|
|
145
160
|
join: wrap(joinReducer),
|
|
146
161
|
part: wrap(partReducer),
|
|
147
162
|
topic: wrap(topicReducer),
|