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
|
@@ -4,7 +4,7 @@ import { capReducer } from '../../src/commands/cap';
|
|
|
4
4
|
import { authenticateReducer } from '../../src/commands/sasl';
|
|
5
5
|
import { Effect } from '../../src/effects';
|
|
6
6
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
7
|
-
import type { AccountStore, SaslPayload, SaslResult } from '../../src/ports';
|
|
7
|
+
import type { AccountStore, MtlsIdentityProvider, SaslPayload, SaslResult } from '../../src/ports';
|
|
8
8
|
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
9
9
|
import { encodeBase64 } from '../../src/protocol/base64';
|
|
10
10
|
import type { IrcMessage } from '../../src/protocol/messages';
|
|
@@ -40,7 +40,11 @@ function saslReadyState(): ConnectionState {
|
|
|
40
40
|
return s;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function makeCtx(
|
|
43
|
+
function makeCtx(
|
|
44
|
+
state: ConnectionState,
|
|
45
|
+
accounts?: AccountStore,
|
|
46
|
+
mtlsIdentity?: MtlsIdentityProvider,
|
|
47
|
+
): Ctx {
|
|
44
48
|
return buildCtx({
|
|
45
49
|
serverConfig,
|
|
46
50
|
clock: new FakeClock(1_000),
|
|
@@ -48,6 +52,7 @@ function makeCtx(state: ConnectionState, accounts?: AccountStore): Ctx {
|
|
|
48
52
|
motd: EmptyMotdProvider,
|
|
49
53
|
connection: state,
|
|
50
54
|
...(accounts !== undefined ? { accounts } : {}),
|
|
55
|
+
...(mtlsIdentity !== undefined ? { mtlsIdentity } : {}),
|
|
51
56
|
});
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -61,6 +66,19 @@ class FakeAccountStore implements AccountStore {
|
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/** A fake MtlsIdentityProvider that returns a canned identity for one connId. */
|
|
70
|
+
class FakeMtlsIdentityProvider implements MtlsIdentityProvider {
|
|
71
|
+
readonly calls: string[] = [];
|
|
72
|
+
constructor(
|
|
73
|
+
private readonly connId: string,
|
|
74
|
+
private readonly identity: string | undefined,
|
|
75
|
+
) {}
|
|
76
|
+
getIdentity(connId: string): string | undefined {
|
|
77
|
+
this.calls.push(connId);
|
|
78
|
+
return connId === this.connId ? this.identity : undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
/** Builds the PLAIN base64 payload `authzid\0authcid\0password`. */
|
|
65
83
|
function plainPayload(authcid: string, password: string, authzid = ''): string {
|
|
66
84
|
return encodeBase64(`${authzid}\0${authcid}\0${password}`);
|
|
@@ -243,25 +261,103 @@ describe('authenticateReducer — no AccountStore configured', () => {
|
|
|
243
261
|
});
|
|
244
262
|
|
|
245
263
|
// ============================================================================
|
|
246
|
-
// AUTHENTICATE EXTERNAL —
|
|
264
|
+
// AUTHENTICATE EXTERNAL — mTLS-backed SASL EXTERNAL
|
|
247
265
|
// ============================================================================
|
|
248
266
|
|
|
249
|
-
describe('authenticateReducer — EXTERNAL
|
|
250
|
-
it('
|
|
267
|
+
describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
268
|
+
it('enters the payload phase when an mTLS provider is configured', () => {
|
|
269
|
+
const state = saslReadyState();
|
|
270
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
271
|
+
const ctx = makeCtx(state, undefined, provider);
|
|
272
|
+
const out = authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
273
|
+
|
|
274
|
+
expect(state.saslMech).toBe('EXTERNAL');
|
|
275
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([L('AUTHENTICATE +')]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns 908 ERR_SASLMECHS when no mTLS provider is configured', () => {
|
|
251
279
|
const state = saslReadyState();
|
|
252
280
|
const ctx = makeCtx(state);
|
|
253
281
|
const out = authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
254
282
|
|
|
255
283
|
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
256
|
-
L(`:${SV}
|
|
284
|
+
L(`:${SV} 908 alice PLAIN :are the available SASL mechanisms`),
|
|
257
285
|
]);
|
|
286
|
+
expect(state.saslMech).toBeUndefined();
|
|
258
287
|
});
|
|
259
288
|
|
|
260
|
-
it('
|
|
289
|
+
it('succeeds (900/903) when a valid cert identity is presented', () => {
|
|
261
290
|
const state = saslReadyState();
|
|
262
|
-
const
|
|
291
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
292
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
293
|
+
const ctx = makeCtx(state, accounts, provider);
|
|
294
|
+
|
|
263
295
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
296
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
297
|
+
|
|
298
|
+
expect(state.account).toBe('alice');
|
|
299
|
+
expect(accounts.calls).toEqual([
|
|
300
|
+
{ mech: 'EXTERNAL', payload: { kind: 'EXTERNAL', identity: 'CN=alice' } },
|
|
301
|
+
]);
|
|
302
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
303
|
+
L(`:${SV} 900 alice alice!alice@example.com alice :You are now logged in as alice`),
|
|
304
|
+
L(`:${SV} 903 alice :SASL authentication successful`),
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('clears saslMech/saslBuffer after a successful EXTERNAL', () => {
|
|
309
|
+
const state = saslReadyState();
|
|
310
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
311
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
312
|
+
const ctx = makeCtx(state, accounts, provider);
|
|
313
|
+
|
|
314
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
315
|
+
authenticateReducer(state, authenticate('+'), ctx);
|
|
316
|
+
|
|
264
317
|
expect(state.saslMech).toBeUndefined();
|
|
318
|
+
expect(state.saslBuffer).toBeUndefined();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('fails with 904 when the provider has no identity for the connection', () => {
|
|
322
|
+
const state = saslReadyState();
|
|
323
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
324
|
+
const provider = new FakeMtlsIdentityProvider('c1', undefined);
|
|
325
|
+
const ctx = makeCtx(state, accounts, provider);
|
|
326
|
+
|
|
327
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
328
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
329
|
+
|
|
330
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
331
|
+
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
332
|
+
]);
|
|
333
|
+
expect(state.account).toBeUndefined();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('fails with 904 when the account store rejects the identity', () => {
|
|
337
|
+
const state = saslReadyState();
|
|
338
|
+
const accounts = new FakeAccountStore({ ok: false, reason: 'untrusted certificate' });
|
|
339
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=evil');
|
|
340
|
+
const ctx = makeCtx(state, accounts, provider);
|
|
341
|
+
|
|
342
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
343
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
344
|
+
|
|
345
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
346
|
+
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
347
|
+
]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('fails with 904 when no account store is configured', () => {
|
|
351
|
+
const state = saslReadyState();
|
|
352
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
353
|
+
const ctx = makeCtx(state, undefined, provider);
|
|
354
|
+
|
|
355
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
356
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
357
|
+
|
|
358
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
359
|
+
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
360
|
+
]);
|
|
265
361
|
});
|
|
266
362
|
});
|
|
267
363
|
|
|
@@ -275,6 +371,17 @@ describe('authenticateReducer — unknown mechanism', () => {
|
|
|
275
371
|
const ctx = makeCtx(state);
|
|
276
372
|
const out = authenticateReducer(state, authenticate('CRAM-MD5'), ctx);
|
|
277
373
|
|
|
374
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
375
|
+
L(`:${SV} 908 alice PLAIN :are the available SASL mechanisms`),
|
|
376
|
+
]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('lists PLAIN,EXTERNAL in 908 when an mTLS provider is configured', () => {
|
|
380
|
+
const state = saslReadyState();
|
|
381
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
382
|
+
const ctx = makeCtx(state, undefined, provider);
|
|
383
|
+
const out = authenticateReducer(state, authenticate('CRAM-MD5'), ctx);
|
|
384
|
+
|
|
278
385
|
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
279
386
|
L(`:${SV} 908 alice PLAIN,EXTERNAL :are the available SASL mechanisms`),
|
|
280
387
|
]);
|
|
@@ -529,8 +636,9 @@ describe('authenticateReducer — CAP REQ sasl integration', () => {
|
|
|
529
636
|
expect(state.account).toBe('alice');
|
|
530
637
|
});
|
|
531
638
|
|
|
532
|
-
it('
|
|
639
|
+
it('advertises the base sasl cap value (PLAIN) without mTLS', () => {
|
|
533
640
|
const advertised = getLsString();
|
|
534
|
-
expect(advertised).toContain('sasl=PLAIN
|
|
641
|
+
expect(advertised).toContain('sasl=PLAIN');
|
|
642
|
+
expect(advertised).not.toContain('sasl=PLAIN,EXTERNAL');
|
|
535
643
|
});
|
|
536
644
|
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
adminReducer,
|
|
4
|
+
infoReducer,
|
|
5
|
+
timeReducer,
|
|
6
|
+
versionReducer,
|
|
7
|
+
} from '../../src/commands/server-info';
|
|
8
|
+
import { DEFAULT_SERVER_VERSION } from '../../src/config';
|
|
9
|
+
import { Effect } from '../../src/effects';
|
|
10
|
+
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
11
|
+
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
12
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
13
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
14
|
+
|
|
15
|
+
const baseServerConfig: ServerConfig = {
|
|
16
|
+
serverName: 'irc.example.com',
|
|
17
|
+
networkName: 'ExampleNet',
|
|
18
|
+
maxChannelsPerUser: 30,
|
|
19
|
+
maxTargetsPerCommand: 10,
|
|
20
|
+
maxListEntries: 50,
|
|
21
|
+
nickLen: 30,
|
|
22
|
+
channelLen: 50,
|
|
23
|
+
topicLen: 390,
|
|
24
|
+
quitMessage: 'Client Quit',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function makeCtx(state: ConnectionState, clockMs = 5_000): Ctx {
|
|
28
|
+
return buildCtx({
|
|
29
|
+
serverConfig: baseServerConfig,
|
|
30
|
+
clock: new FakeClock(clockMs),
|
|
31
|
+
ids: new SequentialIdFactory(),
|
|
32
|
+
motd: EmptyMotdProvider,
|
|
33
|
+
connection: state,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeState(): ConnectionState {
|
|
38
|
+
const s = createConnection({ id: 'c1', connectedSince: 0 });
|
|
39
|
+
s.nick = 'alice';
|
|
40
|
+
s.user = 'alice';
|
|
41
|
+
s.host = 'example.com';
|
|
42
|
+
s.realname = 'Alice';
|
|
43
|
+
s.registration = 'registered';
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const L = (text: string): RawLine => ({ text });
|
|
48
|
+
|
|
49
|
+
const version = (server?: string) =>
|
|
50
|
+
({ command: 'VERSION', params: server === undefined ? [] : [server], tags: {} }) as const;
|
|
51
|
+
|
|
52
|
+
const time = (server?: string) =>
|
|
53
|
+
({ command: 'TIME', params: server === undefined ? [] : [server], tags: {} }) as const;
|
|
54
|
+
|
|
55
|
+
const admin = (server?: string) =>
|
|
56
|
+
({ command: 'ADMIN', params: server === undefined ? [] : [server], tags: {} }) as const;
|
|
57
|
+
|
|
58
|
+
const info = (server?: string) =>
|
|
59
|
+
({ command: 'INFO', params: server === undefined ? [] : [server], tags: {} }) as const;
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// VERSION
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
describe('versionReducer', () => {
|
|
66
|
+
it('emits 351 RPL_VERSION with the server version and name', () => {
|
|
67
|
+
const state = makeState();
|
|
68
|
+
const ctx = makeCtx(state);
|
|
69
|
+
|
|
70
|
+
const out = versionReducer(state, version(), ctx);
|
|
71
|
+
|
|
72
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
73
|
+
Effect.send('c1', [
|
|
74
|
+
L(`:irc.example.com 351 alice ${DEFAULT_SERVER_VERSION} irc.example.com :ExampleNet`),
|
|
75
|
+
]),
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('updates lastSeen to ctx.clock.now()', () => {
|
|
80
|
+
const state = makeState();
|
|
81
|
+
const ctx = makeCtx(state, 9_999);
|
|
82
|
+
|
|
83
|
+
versionReducer(state, version(), ctx);
|
|
84
|
+
|
|
85
|
+
expect(state.lastSeen).toBe(9_999);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('uses * as nick placeholder for unregistered connections', () => {
|
|
89
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
90
|
+
const ctx = makeCtx(state);
|
|
91
|
+
|
|
92
|
+
const out = versionReducer(state, version(), ctx);
|
|
93
|
+
|
|
94
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
95
|
+
Effect.send('c1', [
|
|
96
|
+
L(`:irc.example.com 351 * ${DEFAULT_SERVER_VERSION} irc.example.com :ExampleNet`),
|
|
97
|
+
]),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('ignores the optional server target parameter (single-server deployment)', () => {
|
|
102
|
+
const state = makeState();
|
|
103
|
+
const ctx = makeCtx(state);
|
|
104
|
+
|
|
105
|
+
const out = versionReducer(state, version('other.server'), ctx);
|
|
106
|
+
|
|
107
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
108
|
+
Effect.send('c1', [
|
|
109
|
+
L(`:irc.example.com 351 alice ${DEFAULT_SERVER_VERSION} irc.example.com :ExampleNet`),
|
|
110
|
+
]),
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns the same state reference', () => {
|
|
115
|
+
const state = makeState();
|
|
116
|
+
const ctx = makeCtx(state);
|
|
117
|
+
|
|
118
|
+
const out = versionReducer(state, version(), ctx);
|
|
119
|
+
|
|
120
|
+
expect(out.state).toBe(state);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// TIME
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
describe('timeReducer', () => {
|
|
129
|
+
it('emits 391 RPL_TIME with a human-readable timestamp from the clock', () => {
|
|
130
|
+
const state = makeState();
|
|
131
|
+
const ctx = makeCtx(state, 1_700_000_000_000);
|
|
132
|
+
|
|
133
|
+
const out = timeReducer(state, time(), ctx);
|
|
134
|
+
|
|
135
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
136
|
+
Effect.send('c1', [
|
|
137
|
+
L(':irc.example.com 391 alice irc.example.com :2023-11-14T22:13:20.000Z'),
|
|
138
|
+
]),
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('updates lastSeen to ctx.clock.now()', () => {
|
|
143
|
+
const state = makeState();
|
|
144
|
+
const ctx = makeCtx(state, 42_000);
|
|
145
|
+
|
|
146
|
+
timeReducer(state, time(), ctx);
|
|
147
|
+
|
|
148
|
+
expect(state.lastSeen).toBe(42_000);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('uses * as nick placeholder for unregistered connections', () => {
|
|
152
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
153
|
+
const ctx = makeCtx(state, 1_700_000_000_000);
|
|
154
|
+
|
|
155
|
+
const out = timeReducer(state, time(), ctx);
|
|
156
|
+
|
|
157
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
158
|
+
Effect.send('c1', [L(':irc.example.com 391 * irc.example.com :2023-11-14T22:13:20.000Z')]),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('ignores the optional server target parameter', () => {
|
|
163
|
+
const state = makeState();
|
|
164
|
+
const ctx = makeCtx(state, 1_700_000_000_000);
|
|
165
|
+
|
|
166
|
+
const out = timeReducer(state, time('elsewhere'), ctx);
|
|
167
|
+
|
|
168
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
169
|
+
Effect.send('c1', [
|
|
170
|
+
L(':irc.example.com 391 alice irc.example.com :2023-11-14T22:13:20.000Z'),
|
|
171
|
+
]),
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// ADMIN
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
describe('adminReducer', () => {
|
|
181
|
+
it('emits 256 RPL_ADMINME with the server name', () => {
|
|
182
|
+
const state = makeState();
|
|
183
|
+
const ctx = makeCtx(state);
|
|
184
|
+
|
|
185
|
+
const out = adminReducer(state, admin(), ctx);
|
|
186
|
+
|
|
187
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
188
|
+
Effect.send('c1', [L(':irc.example.com 256 alice irc.example.com :Administrative info')]),
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('updates lastSeen to ctx.clock.now()', () => {
|
|
193
|
+
const state = makeState();
|
|
194
|
+
const ctx = makeCtx(state, 7_777);
|
|
195
|
+
|
|
196
|
+
adminReducer(state, admin(), ctx);
|
|
197
|
+
|
|
198
|
+
expect(state.lastSeen).toBe(7_777);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('uses * as nick placeholder for unregistered connections', () => {
|
|
202
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
203
|
+
const ctx = makeCtx(state);
|
|
204
|
+
|
|
205
|
+
const out = adminReducer(state, admin(), ctx);
|
|
206
|
+
|
|
207
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
208
|
+
Effect.send('c1', [L(':irc.example.com 256 * irc.example.com :Administrative info')]),
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('ignores the optional server target parameter', () => {
|
|
213
|
+
const state = makeState();
|
|
214
|
+
const ctx = makeCtx(state);
|
|
215
|
+
|
|
216
|
+
const out = adminReducer(state, admin('foo'), ctx);
|
|
217
|
+
|
|
218
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
219
|
+
Effect.send('c1', [L(':irc.example.com 256 alice irc.example.com :Administrative info')]),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// INFO
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
describe('infoReducer', () => {
|
|
229
|
+
it('emits 371 RPL_INFO lines followed by 374 RPL_ENDOFINFO', () => {
|
|
230
|
+
const state = makeState();
|
|
231
|
+
const ctx = makeCtx(state);
|
|
232
|
+
|
|
233
|
+
const out = infoReducer(state, info(), ctx);
|
|
234
|
+
|
|
235
|
+
const lines = (out.effects[0] as { lines: RawLine[] }).lines;
|
|
236
|
+
expect(lines[0]).toEqual(
|
|
237
|
+
L(`:irc.example.com 371 alice :ServerlessIRCd - version ${DEFAULT_SERVER_VERSION}`),
|
|
238
|
+
);
|
|
239
|
+
expect(lines[1]).toEqual(L(':irc.example.com 371 alice :Running on network ExampleNet'));
|
|
240
|
+
expect(lines[lines.length - 1]).toEqual(L(':irc.example.com 374 alice :End of INFO list'));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('includes at least one 371 line and the 374 terminator', () => {
|
|
244
|
+
const state = makeState();
|
|
245
|
+
const ctx = makeCtx(state);
|
|
246
|
+
|
|
247
|
+
const out = infoReducer(state, info(), ctx);
|
|
248
|
+
|
|
249
|
+
const texts = (out.effects[0] as { lines: RawLine[] }).lines.map((l) => l.text);
|
|
250
|
+
const rpl371Count = texts.filter((t) => / 371 /.test(t)).length;
|
|
251
|
+
const rpl374Count = texts.filter((t) => / 374 /.test(t)).length;
|
|
252
|
+
expect(rpl371Count).toBeGreaterThanOrEqual(1);
|
|
253
|
+
expect(rpl374Count).toBe(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('updates lastSeen to ctx.clock.now()', () => {
|
|
257
|
+
const state = makeState();
|
|
258
|
+
const ctx = makeCtx(state, 3_333);
|
|
259
|
+
|
|
260
|
+
infoReducer(state, info(), ctx);
|
|
261
|
+
|
|
262
|
+
expect(state.lastSeen).toBe(3_333);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('uses * as nick placeholder for unregistered connections', () => {
|
|
266
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
267
|
+
const ctx = makeCtx(state);
|
|
268
|
+
|
|
269
|
+
const out = infoReducer(state, info(), ctx);
|
|
270
|
+
|
|
271
|
+
const texts = (out.effects[0] as { lines: RawLine[] }).lines.map((l) => l.text);
|
|
272
|
+
expect(texts.every((t) => t.includes(' * '))).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
});
|