serverless-ircd 0.2.0 → 0.4.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 +258 -18
- package/README.md +72 -30
- package/apps/aws-stack/README.md +2 -2
- 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 +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- 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/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +11 -1
- package/apps/cf-worker/wrangler.toml +69 -19
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +11 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +46 -3
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- package/package.json +19 -16
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +124 -34
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +62 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +43 -2
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +120 -9
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +13 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
- package/packages/aws-adapter/tests/account-store.test.ts +279 -0
- 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/config-loader.test.ts +75 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +194 -47
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- 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 +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +40 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +62 -0
- package/packages/cf-adapter/src/connection-do.ts +181 -31
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +58 -8
- package/packages/cf-adapter/src/index.ts +11 -9
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- 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.test.ts +40 -0
- 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/worker/main.ts +9 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +8 -2
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- package/packages/irc-core/src/case-fold.ts +64 -0
- 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 +11 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +4 -3
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +26 -19
- 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/tagmsg.ts +205 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +84 -3
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +240 -7
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +60 -1
- package/packages/irc-core/tests/account-store.test.ts +131 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- 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/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +256 -19
- 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/tagmsg.test.ts +662 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +170 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- 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 +182 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +21 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +617 -1
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.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 +78 -13
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +132 -2
- 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 +9 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +79 -0
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/docs/AWS-Adapter-Architecture.md +0 -494
- package/docs/AWS-Deployment.md +0 -1107
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -1
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -440
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
|
+
type AccountStore,
|
|
29
30
|
type ChanName,
|
|
30
31
|
type ChannelState,
|
|
31
32
|
type Clock,
|
|
33
|
+
type ConnSnapshot,
|
|
32
34
|
type ConnectionState,
|
|
33
35
|
Effect,
|
|
34
36
|
type Effect as EffectType,
|
|
@@ -38,6 +40,8 @@ import {
|
|
|
38
40
|
type Logger,
|
|
39
41
|
type MessageStore,
|
|
40
42
|
type MotdProvider,
|
|
43
|
+
type MtlsIdentityProvider,
|
|
44
|
+
type NickHistoryStore,
|
|
41
45
|
NoopLoggerInstance,
|
|
42
46
|
Numerics,
|
|
43
47
|
type RawLine,
|
|
@@ -46,15 +50,20 @@ import {
|
|
|
46
50
|
chathistoryReducer,
|
|
47
51
|
handleJoinZero,
|
|
48
52
|
isChannelTarget,
|
|
53
|
+
isonReducer,
|
|
49
54
|
listReducer,
|
|
50
55
|
parse,
|
|
56
|
+
toSnapshot,
|
|
57
|
+
userhostReducer,
|
|
51
58
|
whoReducer,
|
|
52
59
|
whoisReducer,
|
|
60
|
+
whowasReducer,
|
|
53
61
|
} from '@serverless-ircd/irc-core';
|
|
54
62
|
import { dispatch } from './dispatch.js';
|
|
55
63
|
import type { DispatchOptions } from './dispatch.js';
|
|
56
64
|
import { type RoutedReducers, buildRoutedReducers } from './routing.js';
|
|
57
65
|
import type { IrcRuntime } from './runtime.js';
|
|
66
|
+
import { type Transport, WsTextFrameTransport } from './transport.js';
|
|
58
67
|
|
|
59
68
|
/**
|
|
60
69
|
* Channel-state access for the actor.
|
|
@@ -103,6 +112,32 @@ export interface ConnectionActorOptions {
|
|
|
103
112
|
* opt in.
|
|
104
113
|
*/
|
|
105
114
|
readonly messages?: MessageStore;
|
|
115
|
+
/**
|
|
116
|
+
* SASL account verification source (absent when no accounts are
|
|
117
|
+
* configured). When bound, `ctx.accounts` is defined so the
|
|
118
|
+
* `AUTHENTICATE` reducer can verify PLAIN credentials end-to-end.
|
|
119
|
+
* Omitted (→ `ctx.accounts` undefined) preserves the existing
|
|
120
|
+
* behaviour for callers/tests that do not opt in: a client completing
|
|
121
|
+
* `AUTHENTICATE PLAIN` receives `904 ERR_SASLFAIL` because no store
|
|
122
|
+
* can vouch for the credentials.
|
|
123
|
+
*/
|
|
124
|
+
readonly accounts?: AccountStore;
|
|
125
|
+
/**
|
|
126
|
+
* mTLS client-cert identity source for SASL EXTERNAL (absent when mTLS
|
|
127
|
+
* is not configured). When bound, `ctx.mtlsIdentity` is defined so the
|
|
128
|
+
* `AUTHENTICATE EXTERNAL` reducer can resolve the connection's verified
|
|
129
|
+
* certificate subject. Omitted (→ `ctx.mtlsIdentity` undefined) means
|
|
130
|
+
* EXTERNAL is rejected with `904 ERR_SASLFAIL`.
|
|
131
|
+
*/
|
|
132
|
+
readonly mtlsIdentity?: MtlsIdentityProvider;
|
|
133
|
+
/**
|
|
134
|
+
* Nick-history source for the `WHOWAS` command (absent when WHOWAS
|
|
135
|
+
* history retention is disabled). When bound, `ctx.history` is defined
|
|
136
|
+
* so the QUIT and NICK-change reducers record sign-off entries and the
|
|
137
|
+
* WHOWAS reducer replays them. Omitted (→ `ctx.history` undefined)
|
|
138
|
+
* preserves the no-store behaviour: `WHOWAS <nick>` emits `406`+`369`.
|
|
139
|
+
*/
|
|
140
|
+
readonly history?: NickHistoryStore;
|
|
106
141
|
/**
|
|
107
142
|
* Structured-logger port. When omitted the actor runs
|
|
108
143
|
* silently — observability is purely opt-in. When supplied, every
|
|
@@ -116,6 +151,15 @@ export interface ConnectionActorOptions {
|
|
|
116
151
|
* still uniquely identifiable in logs.
|
|
117
152
|
*/
|
|
118
153
|
readonly traceId?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Line-framing seam (PLAN §9, ADR-009). Turns inbound transport input
|
|
156
|
+
* into complete IRC lines. Defaults to a {@link WsTextFrameTransport}
|
|
157
|
+
* (one message per WebSocket text frame, the serverless default). A raw
|
|
158
|
+
* TCP+TLS adapter injects a {@link TcpByteStreamTransport} for stateful
|
|
159
|
+
* `\r\n` byte-stream framing across chunks. The parser/reducer/dispatch
|
|
160
|
+
* pipeline is shared unchanged across both transports.
|
|
161
|
+
*/
|
|
162
|
+
readonly transport?: Transport;
|
|
119
163
|
}
|
|
120
164
|
|
|
121
165
|
export class ConnectionActor {
|
|
@@ -127,7 +171,17 @@ export class ConnectionActor {
|
|
|
127
171
|
private readonly ids: IdFactory;
|
|
128
172
|
private readonly motd: MotdProvider;
|
|
129
173
|
private readonly messages: MessageStore | undefined;
|
|
174
|
+
private readonly accounts: AccountStore | undefined;
|
|
175
|
+
private readonly mtlsIdentity: MtlsIdentityProvider | undefined;
|
|
176
|
+
private readonly history: NickHistoryStore | undefined;
|
|
130
177
|
private readonly reducers: RoutedReducers;
|
|
178
|
+
/**
|
|
179
|
+
* Line-framing seam. Turns inbound input (a WS text frame or a raw TCP
|
|
180
|
+
* chunk) into complete IRC lines. Defaulted to a WS-frame transport for
|
|
181
|
+
* the serverless default; a TCP+TLS adapter injects the stateful
|
|
182
|
+
* byte-stream transport at construction.
|
|
183
|
+
*/
|
|
184
|
+
private readonly transport: Transport;
|
|
131
185
|
/**
|
|
132
186
|
* Bound logger. Defaults to a no-op so the actor pipeline has no
|
|
133
187
|
* observable side effects when callers do not opt in.
|
|
@@ -150,6 +204,9 @@ export class ConnectionActor {
|
|
|
150
204
|
this.ids = opts.ids;
|
|
151
205
|
this.motd = opts.motd ?? EmptyMotdProvider;
|
|
152
206
|
this.messages = opts.messages;
|
|
207
|
+
this.accounts = opts.accounts;
|
|
208
|
+
this.mtlsIdentity = opts.mtlsIdentity;
|
|
209
|
+
this.history = opts.history;
|
|
153
210
|
this.logger = opts.logger ?? NoopLoggerInstance;
|
|
154
211
|
this.pinnedTraceId = opts.traceId;
|
|
155
212
|
// Bind the connection id once so every record the actor emits carries
|
|
@@ -163,15 +220,19 @@ export class ConnectionActor {
|
|
|
163
220
|
// reducer when flood control is disabled) so a flooding client is bounded
|
|
164
221
|
// regardless of which authority owns the command's state.
|
|
165
222
|
this.reducers = buildRoutedReducers(opts.serverConfig);
|
|
223
|
+
this.transport = opts.transport ?? new WsTextFrameTransport();
|
|
166
224
|
}
|
|
167
225
|
|
|
168
226
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
227
|
+
* Transport-agnostic entry point: feeds `chunk` through the bound
|
|
228
|
+
* {@link Transport} and dispatches each complete, non-empty line in order.
|
|
229
|
+
* A WS adapter passes one text frame per call; a raw TCP+TLS adapter
|
|
230
|
+
* passes one arbitrary byte chunk per call and the transport buffers any
|
|
231
|
+
* partial trailing line across calls. A parse failure on one line emits a
|
|
232
|
+
* `421` and processing continues with the next line.
|
|
172
233
|
*/
|
|
173
|
-
async
|
|
174
|
-
const lines =
|
|
234
|
+
async receive(chunk: string): Promise<void> {
|
|
235
|
+
const lines = this.transport.feed(chunk);
|
|
175
236
|
if (lines.length === 0) return;
|
|
176
237
|
const traceId = this.pinnedTraceId ?? this.ids.traceId();
|
|
177
238
|
const log = this.logger.child({ traceId });
|
|
@@ -181,6 +242,17 @@ export class ConnectionActor {
|
|
|
181
242
|
}
|
|
182
243
|
}
|
|
183
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Processes one WebSocket text frame. Equivalent to {@link receive} — kept
|
|
247
|
+
* as the named entry point every WS adapter already calls. Splits on
|
|
248
|
+
* `\r\n` (tolerating bare `\n`) via the default WS-frame transport and
|
|
249
|
+
* dispatches each non-empty line in order. A parse failure on one line
|
|
250
|
+
* emits a `421` and processing continues with the next line.
|
|
251
|
+
*/
|
|
252
|
+
async receiveTextFrame(text: string): Promise<void> {
|
|
253
|
+
await this.receive(text);
|
|
254
|
+
}
|
|
255
|
+
|
|
184
256
|
private async processLine(line: string, log: Logger, traceId: string): Promise<void> {
|
|
185
257
|
let msg: IrcMessage;
|
|
186
258
|
let parseFailed = false;
|
|
@@ -242,6 +314,9 @@ export class ConnectionActor {
|
|
|
242
314
|
motd: this.motd,
|
|
243
315
|
connection: this.state,
|
|
244
316
|
...(this.messages !== undefined ? { messages: this.messages } : {}),
|
|
317
|
+
...(this.accounts !== undefined ? { accounts: this.accounts } : {}),
|
|
318
|
+
...(this.mtlsIdentity !== undefined ? { mtlsIdentity: this.mtlsIdentity } : {}),
|
|
319
|
+
...(this.history !== undefined ? { history: this.history } : {}),
|
|
245
320
|
});
|
|
246
321
|
|
|
247
322
|
switch (msg.command) {
|
|
@@ -260,6 +335,8 @@ export class ConnectionActor {
|
|
|
260
335
|
return this.reducers.quit(this.state, msg, ctx).effects;
|
|
261
336
|
case 'MOTD':
|
|
262
337
|
return this.reducers.motd(this.state, msg, ctx).effects;
|
|
338
|
+
case 'OPER':
|
|
339
|
+
return this.reducers.oper(this.state, msg, ctx).effects;
|
|
263
340
|
case 'CAP':
|
|
264
341
|
return this.reducers.cap(this.state, msg, ctx).effects;
|
|
265
342
|
case 'AUTHENTICATE':
|
|
@@ -267,6 +344,20 @@ export class ConnectionActor {
|
|
|
267
344
|
case 'AWAY':
|
|
268
345
|
return this.reducers.away(this.state, msg, ctx).effects;
|
|
269
346
|
|
|
347
|
+
// -------- Server-info query commands --------
|
|
348
|
+
case 'VERSION':
|
|
349
|
+
return this.reducers.version(this.state, msg, ctx).effects;
|
|
350
|
+
case 'TIME':
|
|
351
|
+
return this.reducers.time(this.state, msg, ctx).effects;
|
|
352
|
+
case 'ADMIN':
|
|
353
|
+
return this.reducers.admin(this.state, msg, ctx).effects;
|
|
354
|
+
case 'INFO':
|
|
355
|
+
return this.reducers.info(this.state, msg, ctx).effects;
|
|
356
|
+
case 'USERHOST':
|
|
357
|
+
return this.routeUserhost(msg, ctx);
|
|
358
|
+
case 'ISON':
|
|
359
|
+
return this.routeIson(msg, ctx);
|
|
360
|
+
|
|
270
361
|
// -------- Channel-authority commands --------
|
|
271
362
|
case 'JOIN':
|
|
272
363
|
return this.routeJoin(msg, ctx);
|
|
@@ -289,6 +380,8 @@ export class ConnectionActor {
|
|
|
289
380
|
return this.routeWho(msg, ctx);
|
|
290
381
|
case 'WHOIS':
|
|
291
382
|
return this.routeWhois(msg, ctx);
|
|
383
|
+
case 'WHOWAS':
|
|
384
|
+
return this.routeWhowas(msg, ctx);
|
|
292
385
|
case 'CHATHISTORY':
|
|
293
386
|
return this.routeChathistory(msg, ctx);
|
|
294
387
|
|
|
@@ -307,6 +400,13 @@ export class ConnectionActor {
|
|
|
307
400
|
this.reducers.noticeUser,
|
|
308
401
|
ctx,
|
|
309
402
|
);
|
|
403
|
+
case 'TAGMSG':
|
|
404
|
+
return this.routeMessageTarget(
|
|
405
|
+
msg,
|
|
406
|
+
this.reducers.tagmsgChannel,
|
|
407
|
+
this.reducers.tagmsgUser,
|
|
408
|
+
ctx,
|
|
409
|
+
);
|
|
310
410
|
case 'MODE':
|
|
311
411
|
return isChannelTarget(msg.params[0] ?? '')
|
|
312
412
|
? this.routeSingleChannel(
|
|
@@ -316,6 +416,16 @@ export class ConnectionActor {
|
|
|
316
416
|
: this.reducers.userMode(this.state, msg, ctx).effects;
|
|
317
417
|
|
|
318
418
|
default:
|
|
419
|
+
// Remaining standard IRC verbs not yet implemented. They fall through
|
|
420
|
+
// to a graceful `421 ERR_UNKNOWNCOMMAND`. Triage of remaining verbs:
|
|
421
|
+
// - Deferred (serverless-incompatible / S2S / needs new infra):
|
|
422
|
+
// LUSERS (needs cross-adapter getStats), KILL (oper disconnect),
|
|
423
|
+
// REHASH (no runtime config reload), CONNECT/SQUIT (S2S, PLAN
|
|
424
|
+
// non-goal), TRACE (complex routing info), STATS (stats backend),
|
|
425
|
+
// WALLOPS (oper broadcast mechanism), SETNAME (CHGHOST plumbing),
|
|
426
|
+
// LINKS (S2S server list).
|
|
427
|
+
// - Obsolete (RFC 2812, never widely implemented): SERVICE, SUMMON,
|
|
428
|
+
// USERS.
|
|
319
429
|
return [this.unknownCommandEffect(msg.command)];
|
|
320
430
|
}
|
|
321
431
|
}
|
|
@@ -364,6 +474,72 @@ export class ConnectionActor {
|
|
|
364
474
|
return whoisReducer(target, channels, msg, ctx).effects;
|
|
365
475
|
}
|
|
366
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Routes `WHOWAS <nick>{,<nick>} [<count>]` to the whowas reducer. The
|
|
479
|
+
* command is query-only: it reads the bound nick-history store
|
|
480
|
+
* (`this.history`, absent when no store is wired) and emits
|
|
481
|
+
* `314`/`369`/`406` numerics. No runtime lookups are needed — the store
|
|
482
|
+
* is synchronous and self-contained.
|
|
483
|
+
*/
|
|
484
|
+
private async routeWhowas(
|
|
485
|
+
msg: IrcMessage,
|
|
486
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
487
|
+
): Promise<EffectType[]> {
|
|
488
|
+
return whowasReducer(this.history, msg, ctx).effects;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Routes `USERHOST <nick>{ <nick>}` to the userhost reducer. Resolves each
|
|
493
|
+
* requested nick via the nick registry + connection lookup, passing the
|
|
494
|
+
* resulting snapshots (or `null` for offline nicks) to the pure formatter.
|
|
495
|
+
* The number of queried nicks is capped at
|
|
496
|
+
* `serverConfig.maxTargetsPerCommand` by the reducer.
|
|
497
|
+
*/
|
|
498
|
+
private async routeUserhost(
|
|
499
|
+
msg: IrcMessage,
|
|
500
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
501
|
+
): Promise<EffectType[]> {
|
|
502
|
+
const cap = ctx.serverConfig.maxTargetsPerCommand;
|
|
503
|
+
const nicks = msg.params.slice(0, cap);
|
|
504
|
+
const resolved = new Map<string, ConnSnapshot | null>();
|
|
505
|
+
for (const nick of nicks) {
|
|
506
|
+
if (nick.length === 0) continue;
|
|
507
|
+
const connId = await this.runtime.lookupNick(nick);
|
|
508
|
+
if (connId === null) {
|
|
509
|
+
resolved.set(nick, null);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const conn = await this.runtime.getConnection(connId);
|
|
513
|
+
resolved.set(nick, conn === null ? null : toSnapshot(conn));
|
|
514
|
+
}
|
|
515
|
+
return userhostReducer(resolved, msg, ctx).effects;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Routes `ISON <nick>{ <nick>}` to the ison reducer. Resolves each requested
|
|
520
|
+
* nick via the nick registry; for online nicks, fetches the connection to
|
|
521
|
+
* recover the registered (display) spelling. Passes a folded→display map to
|
|
522
|
+
* the pure formatter.
|
|
523
|
+
*/
|
|
524
|
+
private async routeIson(
|
|
525
|
+
msg: IrcMessage,
|
|
526
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
527
|
+
): Promise<EffectType[]> {
|
|
528
|
+
const cap = ctx.serverConfig.maxTargetsPerCommand;
|
|
529
|
+
const nicks = msg.params.slice(0, cap);
|
|
530
|
+
const online = new Map<string, string>();
|
|
531
|
+
for (const nick of nicks) {
|
|
532
|
+
if (nick.length === 0) continue;
|
|
533
|
+
const connId = await this.runtime.lookupNick(nick);
|
|
534
|
+
if (connId === null) continue;
|
|
535
|
+
const conn = await this.runtime.getConnection(connId);
|
|
536
|
+
if (conn?.nick !== undefined) {
|
|
537
|
+
online.set(conn.nick.toLowerCase(), conn.nick);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return isonReducer(online, msg, ctx).effects;
|
|
541
|
+
}
|
|
542
|
+
|
|
367
543
|
/**
|
|
368
544
|
* Routes `CHATHISTORY <sub> <target> [<args>...]` to the chathistory
|
|
369
545
|
* reducer. The command is query-only and must NOT create a channel as a
|
|
@@ -508,15 +684,6 @@ export class ConnectionActor {
|
|
|
508
684
|
}
|
|
509
685
|
}
|
|
510
686
|
|
|
511
|
-
/**
|
|
512
|
-
* Splits a WebSocket text frame into IRC lines. Tolerates `\r\n`, bare `\n`,
|
|
513
|
-
* and missing trailing terminators. Returns each line bare (no CRLF).
|
|
514
|
-
*/
|
|
515
|
-
export function splitFrameLines(text: string): string[] {
|
|
516
|
-
if (text.length === 0) return [];
|
|
517
|
-
return text.split(/\r?\n/u);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
687
|
/** Returns the first whitespace-delimited token of `line`, or `''`. */
|
|
521
688
|
function firstToken(line: string): string {
|
|
522
689
|
const m = line.match(/^\S+/u);
|
|
@@ -557,6 +724,7 @@ function channelTargetsForMessage(msg: IrcMessage): ChanName[] {
|
|
|
557
724
|
case 'MODE':
|
|
558
725
|
case 'PRIVMSG':
|
|
559
726
|
case 'NOTICE':
|
|
727
|
+
case 'TAGMSG':
|
|
560
728
|
return splitCsv(msg.params[0] ?? '').filter(isChannelTarget) as ChanName[];
|
|
561
729
|
case 'INVITE':
|
|
562
730
|
// INVITE <nick> <channel> — the channel is the second param.
|
|
@@ -33,9 +33,6 @@ 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
|
-
LookupNick: (e, r) => r.lookupNick(e.nick).then(noop),
|
|
37
|
-
GetConnectionInfo: (e, r) => r.getConnectionInfo(e.conn).then(noop),
|
|
38
|
-
GetChannelSnapshot: (e, r) => r.getChannelSnapshot(e.chan).then(noop),
|
|
39
36
|
};
|
|
40
37
|
|
|
41
38
|
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,
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
nickReducer,
|
|
43
45
|
noticeChannelReducer,
|
|
44
46
|
noticeUserReducer,
|
|
47
|
+
operReducer,
|
|
45
48
|
partReducer,
|
|
46
49
|
passReducer,
|
|
47
50
|
pingReducer,
|
|
@@ -49,9 +52,13 @@ import {
|
|
|
49
52
|
privmsgChannelReducer,
|
|
50
53
|
privmsgUserReducer,
|
|
51
54
|
quitReducer,
|
|
55
|
+
tagmsgChannelReducer,
|
|
56
|
+
tagmsgUserReducer,
|
|
57
|
+
timeReducer,
|
|
52
58
|
topicReducer,
|
|
53
59
|
userModeReducer,
|
|
54
60
|
userReducer,
|
|
61
|
+
versionReducer,
|
|
55
62
|
wrapWithFloodControl,
|
|
56
63
|
} from '@serverless-ircd/irc-core';
|
|
57
64
|
|
|
@@ -69,9 +76,14 @@ export interface RoutedReducers {
|
|
|
69
76
|
readonly pass: Reducer<ConnectionState>;
|
|
70
77
|
readonly quit: Reducer<ConnectionState>;
|
|
71
78
|
readonly motd: Reducer<ConnectionState>;
|
|
79
|
+
readonly oper: Reducer<ConnectionState>;
|
|
72
80
|
readonly cap: Reducer<ConnectionState>;
|
|
73
81
|
readonly authenticate: Reducer<ConnectionState>;
|
|
74
82
|
readonly away: Reducer<ConnectionState>;
|
|
83
|
+
readonly version: Reducer<ConnectionState>;
|
|
84
|
+
readonly time: Reducer<ConnectionState>;
|
|
85
|
+
readonly admin: Reducer<ConnectionState>;
|
|
86
|
+
readonly info: Reducer<ConnectionState>;
|
|
75
87
|
// Channel-authority reducers.
|
|
76
88
|
readonly join: Reducer<ChannelState>;
|
|
77
89
|
readonly part: Reducer<ChannelState>;
|
|
@@ -84,6 +96,8 @@ export interface RoutedReducers {
|
|
|
84
96
|
readonly privmsgUser: Reducer<ConnectionState>;
|
|
85
97
|
readonly noticeChannel: Reducer<ChannelState>;
|
|
86
98
|
readonly noticeUser: Reducer<ConnectionState>;
|
|
99
|
+
readonly tagmsgChannel: Reducer<ChannelState>;
|
|
100
|
+
readonly tagmsgUser: Reducer<ConnectionState>;
|
|
87
101
|
readonly channelMode: Reducer<ChannelState>;
|
|
88
102
|
readonly userMode: Reducer<ConnectionState>;
|
|
89
103
|
}
|
|
@@ -132,9 +146,14 @@ export function buildRoutedReducers(serverConfig: ServerConfig): RoutedReducers
|
|
|
132
146
|
pass: wrap(passReducer),
|
|
133
147
|
quit: wrap(quitReducer),
|
|
134
148
|
motd: wrap(motdReducer),
|
|
149
|
+
oper: wrap(operReducer),
|
|
135
150
|
cap: wrap(capReducer),
|
|
136
151
|
authenticate: wrap(authenticateReducer),
|
|
137
152
|
away: wrap(awayReducer),
|
|
153
|
+
version: wrap(versionReducer),
|
|
154
|
+
time: wrap(timeReducer),
|
|
155
|
+
admin: wrap(adminReducer),
|
|
156
|
+
info: wrap(infoReducer),
|
|
138
157
|
join: wrap(joinReducer),
|
|
139
158
|
part: wrap(partReducer),
|
|
140
159
|
topic: wrap(topicReducer),
|
|
@@ -145,6 +164,8 @@ export function buildRoutedReducers(serverConfig: ServerConfig): RoutedReducers
|
|
|
145
164
|
privmsgUser: wrap(privmsgUserReducer),
|
|
146
165
|
noticeChannel: wrap(noticeChannelReducer),
|
|
147
166
|
noticeUser: wrap(noticeUserReducer),
|
|
167
|
+
tagmsgChannel: wrap(tagmsgChannelReducer),
|
|
168
|
+
tagmsgUser: wrap(tagmsgUserReducer),
|
|
148
169
|
channelMode: wrap(channelModeReducer),
|
|
149
170
|
userMode: wrap(userModeReducer),
|
|
150
171
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport seam: turns raw transport input into complete IRC lines.
|
|
3
|
+
*
|
|
4
|
+
* PLAN §9 (ADR-009) — the pure reducer core + `ConnectionActor` are shared
|
|
5
|
+
* across both the WebSocket (`wss`) and the raw TCP+TLS (`irc+tls :6697`)
|
|
6
|
+
* transports. The only transport-specific concern is line framing:
|
|
7
|
+
* - A WebSocket text frame is a complete unit (one or N `\r\n`-joined
|
|
8
|
+
* messages), so a trailing unterminated line is still emitted.
|
|
9
|
+
* - A raw TCP byte stream arrives in arbitrary chunks, so a partial line
|
|
10
|
+
* at a chunk boundary must be buffered until its `\r\n` terminator
|
|
11
|
+
* arrives.
|
|
12
|
+
*
|
|
13
|
+
* Both transports emit complete lines (no terminators); the actor skips
|
|
14
|
+
* empty lines and parses each non-empty one through the shared
|
|
15
|
+
* parser/reducer/dispatch pipeline.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The line-framing contract a {@link ConnectionActor} consumes. `feed`
|
|
20
|
+
* returns the complete IRC lines terminated within `chunk`; a stateful
|
|
21
|
+
* transport buffers any incomplete trailing line for the next call.
|
|
22
|
+
*/
|
|
23
|
+
export interface Transport {
|
|
24
|
+
feed(chunk: string): string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Splits a WebSocket text frame into IRC lines. Tolerates `\r\n`, bare `\n`,
|
|
29
|
+
* and a missing trailing terminator (a WS frame is a complete unit). Returns
|
|
30
|
+
* each line bare (no CRLF). This is the legacy-tolerant WS contract.
|
|
31
|
+
*/
|
|
32
|
+
export function splitFrameLines(text: string): string[] {
|
|
33
|
+
if (text.length === 0) return [];
|
|
34
|
+
return text.split(/\r?\n/u);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Stateless WS-frame transport: one {@link feed} call per inbound text frame.
|
|
39
|
+
* A frame may carry one message or N `\r\n`-joined messages; a trailing
|
|
40
|
+
* unterminated line is emitted because the frame itself is the unit of
|
|
41
|
+
* completeness. This is the default transport for the WebSocket adapters.
|
|
42
|
+
*/
|
|
43
|
+
export class WsTextFrameTransport implements Transport {
|
|
44
|
+
feed(chunk: string): string[] {
|
|
45
|
+
return splitFrameLines(chunk);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stateful TCP byte-stream transport: reassembles `\r\n`- (or bare `\n`-)
|
|
51
|
+
* terminated lines across arbitrary chunks, buffering any partial trailing
|
|
52
|
+
* line until its terminator arrives. Used by the raw TCP+TLS adapter path
|
|
53
|
+
* (PLAN §9). One instance owns one connection's buffer for its lifetime.
|
|
54
|
+
*
|
|
55
|
+
* For long-lived TCP connections fronted by request/response compute (AWS
|
|
56
|
+
* NLB + Lambda) each client chunk arrives in a fresh Lambda
|
|
57
|
+
* invocation. The partial-line buffer must persist across those
|
|
58
|
+
* invocations. {@link getBuffer} / {@link restore} snapshot the in-flight
|
|
59
|
+
* bytes so the adapter can round-trip them through its data store
|
|
60
|
+
* (DynamoDB) between chunks.
|
|
61
|
+
*/
|
|
62
|
+
export class TcpByteStreamTransport implements Transport {
|
|
63
|
+
private buffer = '';
|
|
64
|
+
|
|
65
|
+
feed(chunk: string): string[] {
|
|
66
|
+
if (chunk.length === 0) return [];
|
|
67
|
+
this.buffer += chunk;
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
let start = 0;
|
|
70
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
71
|
+
if (this.buffer[i] === '\n') {
|
|
72
|
+
// A `\n` terminates a line. Strip a preceding `\r` so both `\r\n`
|
|
73
|
+
// and bare `\n` yield the same bare line.
|
|
74
|
+
const lineEnd = i > 0 && this.buffer[i - 1] === '\r' ? i - 1 : i;
|
|
75
|
+
lines.push(this.buffer.slice(start, lineEnd));
|
|
76
|
+
start = i + 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.buffer = this.buffer.slice(start);
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the partial-line bytes currently buffered (not yet terminated).
|
|
85
|
+
* Empty when every fed byte has been emitted as a complete line. The
|
|
86
|
+
* adapter persists this between compute invocations so a chunk split
|
|
87
|
+
* across two requests is reassembled correctly.
|
|
88
|
+
*/
|
|
89
|
+
getBuffer(): string {
|
|
90
|
+
return this.buffer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Replaces the internal buffer with `buffer`. Call before {@link feed}
|
|
95
|
+
* when restoring from a persisted snapshot. An empty (or omitted)
|
|
96
|
+
* `buffer` resets the transport to a fresh state.
|
|
97
|
+
*/
|
|
98
|
+
restore(buffer: string): void {
|
|
99
|
+
this.buffer = buffer;
|
|
100
|
+
}
|
|
101
|
+
}
|