serverless-ircd 0.1.0 → 0.3.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 +96 -2
- package/.github/workflows/deploy-aws.yml +129 -0
- package/.github/workflows/deploy-cf.yml +0 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +436 -0
- package/README.md +127 -84
- package/apps/aws-stack/README.md +73 -0
- package/apps/aws-stack/bin/aws.ts +49 -0
- package/apps/aws-stack/cdk.json +10 -0
- package/apps/aws-stack/package.json +41 -0
- package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
- package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
- package/apps/aws-stack/scripts/smoke.mjs +142 -0
- package/apps/aws-stack/src/aws-stack.ts +263 -0
- package/apps/aws-stack/src/tables.ts +10 -0
- package/apps/aws-stack/tests/localstack.test.ts +46 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
- package/apps/aws-stack/tests/stack.test.ts +464 -0
- package/apps/aws-stack/tsconfig.build.json +11 -0
- package/apps/aws-stack/tsconfig.test.json +8 -0
- package/apps/aws-stack/vitest.config.ts +20 -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 +5 -1
- package/apps/cf-worker/wrangler.toml +66 -17
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +57 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +267 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +126 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +93 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +86 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1186 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +11 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +19 -14
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/aws-runtime.ts +871 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +73 -0
- package/packages/aws-adapter/src/config-loader.ts +126 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +156 -0
- package/packages/aws-adapter/src/dynamo.ts +61 -0
- package/packages/aws-adapter/src/handlers/connect.ts +44 -0
- package/packages/aws-adapter/src/handlers/default.ts +330 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +48 -0
- package/packages/aws-adapter/src/handlers/index.ts +293 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
- package/packages/aws-adapter/src/handlers/state.ts +13 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
- package/packages/aws-adapter/src/index.ts +74 -0
- package/packages/aws-adapter/src/message-store.ts +34 -0
- package/packages/aws-adapter/src/serialize.ts +283 -0
- package/packages/aws-adapter/src/tables.ts +53 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +171 -0
- package/packages/aws-adapter/tests/account-store.test.ts +280 -0
- package/packages/aws-adapter/tests/aws-harness.ts +408 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +213 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -0
- package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
- package/packages/aws-adapter/tests/global-setup.ts +301 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
- package/packages/aws-adapter/tests/handlers.test.ts +473 -0
- package/packages/aws-adapter/tests/message-store.test.ts +55 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
- package/packages/aws-adapter/tests/serialize.test.ts +249 -0
- package/packages/aws-adapter/tests/smoke.test.ts +48 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
- package/packages/aws-adapter/tests/tables.test.ts +74 -0
- package/packages/aws-adapter/tests/transactions.test.ts +446 -0
- package/packages/aws-adapter/tsconfig.build.json +16 -0
- package/packages/aws-adapter/tsconfig.test.json +14 -0
- package/packages/aws-adapter/vitest.config.ts +23 -0
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/cf-runtime.ts +42 -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 +135 -0
- package/packages/cf-adapter/src/connection-do.ts +119 -0
- package/packages/cf-adapter/src/env.ts +27 -1
- package/packages/cf-adapter/src/index.ts +2 -1
- package/packages/cf-adapter/tests/cf-harness.ts +2 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +82 -0
- package/packages/cf-adapter/tests/worker/main.ts +2 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +10 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +107 -16
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +134 -0
- package/packages/irc-core/package.json +13 -2
- package/packages/irc-core/src/admission.ts +216 -0
- package/packages/irc-core/src/caps/capabilities.ts +1 -0
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/cloak.ts +81 -0
- package/packages/irc-core/src/commands/cap.ts +1 -2
- package/packages/irc-core/src/commands/chathistory.ts +305 -0
- package/packages/irc-core/src/commands/index.ts +9 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +63 -10
- 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/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +79 -21
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/config.ts +270 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +7 -0
- package/packages/irc-core/src/ports.ts +719 -0
- package/packages/irc-core/src/protocol/base64.ts +16 -0
- package/packages/irc-core/src/protocol/index.ts +2 -1
- package/packages/irc-core/src/protocol/numerics.ts +11 -0
- package/packages/irc-core/src/protocol/parser.ts +27 -2
- package/packages/irc-core/src/state/connection.ts +41 -0
- package/packages/irc-core/src/types.ts +88 -2
- package/packages/irc-core/stryker.commands.conf.json +41 -0
- package/packages/irc-core/stryker.protocol.conf.json +26 -0
- package/packages/irc-core/tests/account-store.test.ts +88 -0
- package/packages/irc-core/tests/admission.test.ts +229 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- package/packages/irc-core/tests/cloak.test.ts +78 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
- package/packages/irc-core/tests/commands/join.test.ts +246 -2
- 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/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +380 -20
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +688 -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/config.test.ts +721 -0
- package/packages/irc-core/tests/flood-control.test.ts +394 -0
- package/packages/irc-core/tests/message-store.test.ts +530 -0
- package/packages/irc-core/tests/parser.test.ts +44 -1
- package/packages/irc-core/tests/ports.test.ts +263 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +186 -44
- package/packages/irc-server/src/dispatch.ts +89 -5
- package/packages/irc-server/src/index.ts +2 -0
- package/packages/irc-server/src/routing.ts +160 -0
- package/packages/irc-server/tests/actor.test.ts +690 -15
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +204 -0
- package/packages/irc-test-support/README.md +32 -0
- package/packages/irc-test-support/package.json +37 -0
- package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +6 -5
- package/packages/irc-test-support/src/index.ts +24 -0
- package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
- package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
- package/packages/irc-test-support/tests/smoke.test.ts +11 -0
- package/packages/irc-test-support/tsconfig.build.json +16 -0
- package/packages/irc-test-support/tsconfig.test.json +14 -0
- package/packages/irc-test-support/vitest.config.ts +20 -0
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +30 -0
- package/tools/ci-hardening/src/index.ts +6 -0
- package/tools/ci-hardening/src/validate.ts +103 -0
- package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
- package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
- package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
- package/tools/ci-hardening/tests/validate.test.ts +177 -0
- package/tools/ci-hardening/tsconfig.build.json +12 -0
- package/tools/ci-hardening/tsconfig.test.json +10 -0
- package/tools/ci-hardening/vitest.config.ts +21 -0
- package/tools/seed-aws-accounts.ts +80 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
- package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
- package/tools/tcp-ws-forwarder/src/main.ts +33 -14
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
- package/packages/cf-adapter/tests/integration/index.ts +0 -17
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
|
@@ -8,7 +8,14 @@ import {
|
|
|
8
8
|
} from '../../src/commands/privmsg';
|
|
9
9
|
import { Effect } from '../../src/effects';
|
|
10
10
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
EmptyMotdProvider,
|
|
13
|
+
FakeClock,
|
|
14
|
+
InMemoryMessageStore,
|
|
15
|
+
type MessageStore,
|
|
16
|
+
SequentialIdFactory,
|
|
17
|
+
type StoredMessage,
|
|
18
|
+
} from '../../src/ports';
|
|
12
19
|
import { type ChannelState, createChannel } from '../../src/state/channel';
|
|
13
20
|
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
14
21
|
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
@@ -25,13 +32,18 @@ const serverConfig: ServerConfig = {
|
|
|
25
32
|
quitMessage: 'Client Quit',
|
|
26
33
|
};
|
|
27
34
|
|
|
28
|
-
function makeCtx(
|
|
35
|
+
function makeCtx(
|
|
36
|
+
conn: ConnectionState,
|
|
37
|
+
clock = new FakeClock(1_000),
|
|
38
|
+
messages?: MessageStore,
|
|
39
|
+
): Ctx {
|
|
29
40
|
return buildCtx({
|
|
30
41
|
serverConfig,
|
|
31
42
|
clock,
|
|
32
43
|
ids: new SequentialIdFactory(),
|
|
33
44
|
motd: EmptyMotdProvider,
|
|
34
45
|
connection: conn,
|
|
46
|
+
...(messages !== undefined ? { messages } : {}),
|
|
35
47
|
});
|
|
36
48
|
}
|
|
37
49
|
|
|
@@ -663,3 +675,135 @@ describe('noticeUserReducer', () => {
|
|
|
663
675
|
expect(out.effects).toEqual([]);
|
|
664
676
|
});
|
|
665
677
|
});
|
|
678
|
+
|
|
679
|
+
// ============================================================================
|
|
680
|
+
// chathistory recording — MessageStore.record is driven from the channel
|
|
681
|
+
// PRIVMSG/NOTICE reducer for every accepted message.
|
|
682
|
+
// ============================================================================
|
|
683
|
+
|
|
684
|
+
describe('privmsgChannelReducer — chathistory recording', () => {
|
|
685
|
+
it('records the accepted PRIVMSG into ctx.messages with a fresh msgid and time', () => {
|
|
686
|
+
const chan = makeChan('#foo');
|
|
687
|
+
addMember(chan, 'c1', 'alice');
|
|
688
|
+
const store = new InMemoryMessageStore();
|
|
689
|
+
const conn = makeConn();
|
|
690
|
+
const clock = new FakeClock(7_700);
|
|
691
|
+
const ctx = makeCtx(conn, clock, store);
|
|
692
|
+
|
|
693
|
+
privmsgChannelReducer(chan, { command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} }, ctx);
|
|
694
|
+
|
|
695
|
+
const recorded = store.query({ chan: '#foo', direction: 'latest', limit: 10 });
|
|
696
|
+
expect(recorded).toHaveLength(1);
|
|
697
|
+
const entry = recorded[0] as StoredMessage | undefined;
|
|
698
|
+
expect(entry).toBeDefined();
|
|
699
|
+
expect(entry?.msgid).toBe('nonce-0');
|
|
700
|
+
expect(entry?.time).toBe(7_700);
|
|
701
|
+
expect(entry?.chan).toBe('#foo');
|
|
702
|
+
expect(entry?.command).toBe('PRIVMSG');
|
|
703
|
+
expect(entry?.nick).toBe('alice');
|
|
704
|
+
expect(entry?.user).toBe('alice');
|
|
705
|
+
expect(entry?.host).toBe('example.com');
|
|
706
|
+
expect(entry?.text).toBe('hi');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('records an accepted channel NOTICE too', () => {
|
|
710
|
+
const chan = makeChan('#foo');
|
|
711
|
+
addMember(chan, 'c1', 'alice');
|
|
712
|
+
const store = new InMemoryMessageStore();
|
|
713
|
+
const ctx = makeCtx(makeConn(), new FakeClock(1_000), store);
|
|
714
|
+
|
|
715
|
+
noticeChannelReducer(chan, { command: 'NOTICE', params: ['#foo', 'note'], tags: {} }, ctx);
|
|
716
|
+
|
|
717
|
+
const recorded = store.query({ chan: '#foo', direction: 'latest', limit: 10 });
|
|
718
|
+
expect(recorded).toHaveLength(1);
|
|
719
|
+
expect((recorded[0] as StoredMessage).command).toBe('NOTICE');
|
|
720
|
+
expect((recorded[0] as StoredMessage).text).toBe('note');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('does NOT record when ctx.messages is absent (chathistory disabled)', () => {
|
|
724
|
+
const chan = makeChan('#foo');
|
|
725
|
+
addMember(chan, 'c1', 'alice');
|
|
726
|
+
const conn = makeConn();
|
|
727
|
+
const ctx = makeCtx(conn); // no store
|
|
728
|
+
|
|
729
|
+
// Should not throw and should still broadcast.
|
|
730
|
+
const out = privmsgChannelReducer(
|
|
731
|
+
chan,
|
|
732
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
733
|
+
ctx,
|
|
734
|
+
);
|
|
735
|
+
expect(out.effects[0]?.tag).toBe('Broadcast');
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('does NOT record a user-target PRIVMSG (only channel messages are persisted)', () => {
|
|
739
|
+
const store = new InMemoryMessageStore();
|
|
740
|
+
const conn = makeConn();
|
|
741
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), store);
|
|
742
|
+
|
|
743
|
+
privmsgUserReducer(conn, { command: 'PRIVMSG', params: ['bob', 'hi'], tags: {} }, ctx);
|
|
744
|
+
|
|
745
|
+
expect(store.targets(0, 100_000)).toEqual([]);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('does NOT record a user-target NOTICE', () => {
|
|
749
|
+
const store = new InMemoryMessageStore();
|
|
750
|
+
const conn = makeConn();
|
|
751
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), store);
|
|
752
|
+
|
|
753
|
+
noticeUserReducer(conn, { command: 'NOTICE', params: ['bob', 'hi'], tags: {} }, ctx);
|
|
754
|
+
|
|
755
|
+
expect(store.targets(0, 100_000)).toEqual([]);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('does NOT record when the message is blocked by +n (no external messages)', () => {
|
|
759
|
+
const chan = makeChan('#foo');
|
|
760
|
+
chan.modes.noExternal = true;
|
|
761
|
+
// sender c1 is NOT a member
|
|
762
|
+
const store = new InMemoryMessageStore();
|
|
763
|
+
const ctx = makeCtx(makeConn(), new FakeClock(1_000), store);
|
|
764
|
+
|
|
765
|
+
privmsgChannelReducer(chan, { command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} }, ctx);
|
|
766
|
+
|
|
767
|
+
expect(store.query({ chan: '#foo', direction: 'latest', limit: 10 })).toEqual([]);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('does NOT record when the message is blocked by +m (moderated, no voice)', () => {
|
|
771
|
+
const chan = makeChan('#foo');
|
|
772
|
+
chan.modes.moderated = true;
|
|
773
|
+
addMember(chan, 'c1', 'alice', false, false); // not op, not voiced
|
|
774
|
+
const store = new InMemoryMessageStore();
|
|
775
|
+
const ctx = makeCtx(makeConn(), new FakeClock(1_000), store);
|
|
776
|
+
|
|
777
|
+
privmsgChannelReducer(chan, { command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} }, ctx);
|
|
778
|
+
|
|
779
|
+
expect(store.query({ chan: '#foo', direction: 'latest', limit: 10 })).toEqual([]);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('does NOT record when the sender is banned (+b)', () => {
|
|
783
|
+
const chan = makeChan('#foo');
|
|
784
|
+
addMember(chan, 'c1', 'alice');
|
|
785
|
+
chan.banMasks.add('*!*@evil.com');
|
|
786
|
+
const conn = makeConn('c1', 'alice');
|
|
787
|
+
conn.host = 'evil.com';
|
|
788
|
+
const store = new InMemoryMessageStore();
|
|
789
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), store);
|
|
790
|
+
|
|
791
|
+
privmsgChannelReducer(chan, { command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} }, ctx);
|
|
792
|
+
|
|
793
|
+
expect(store.query({ chan: '#foo', direction: 'latest', limit: 10 })).toEqual([]);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('uses a distinct msgid per recorded message (ctx.ids.nonce() each time)', () => {
|
|
797
|
+
const chan = makeChan('#foo');
|
|
798
|
+
addMember(chan, 'c1', 'alice');
|
|
799
|
+
const store = new InMemoryMessageStore();
|
|
800
|
+
const conn = makeConn();
|
|
801
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), store);
|
|
802
|
+
|
|
803
|
+
privmsgChannelReducer(chan, { command: 'PRIVMSG', params: ['#foo', 'one'], tags: {} }, ctx);
|
|
804
|
+
privmsgChannelReducer(chan, { command: 'PRIVMSG', params: ['#foo', 'two'], tags: {} }, ctx);
|
|
805
|
+
|
|
806
|
+
const recorded = store.query({ chan: '#foo', direction: 'latest', limit: 10 });
|
|
807
|
+
expect(recorded.map((m) => m.msgid)).toEqual(['nonce-0', 'nonce-1']);
|
|
808
|
+
});
|
|
809
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { cloakHost } from '../../src/cloak';
|
|
2
3
|
import { generateIsupport } from '../../src/commands/isupport';
|
|
3
4
|
import {
|
|
4
5
|
buildWelcomeLines,
|
|
@@ -10,7 +11,13 @@ import {
|
|
|
10
11
|
} from '../../src/commands/registration';
|
|
11
12
|
import { Effect } from '../../src/effects';
|
|
12
13
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
EmptyMotdProvider,
|
|
16
|
+
FakeClock,
|
|
17
|
+
SequentialIdFactory,
|
|
18
|
+
StaticMotdProvider,
|
|
19
|
+
} from '../../src/ports';
|
|
20
|
+
import type { MotdProvider } from '../../src/ports';
|
|
14
21
|
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
15
22
|
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
16
23
|
|
|
@@ -26,12 +33,16 @@ const serverConfig: ServerConfig = {
|
|
|
26
33
|
quitMessage: 'Client Quit',
|
|
27
34
|
};
|
|
28
35
|
|
|
29
|
-
function makeCtx(
|
|
36
|
+
function makeCtx(
|
|
37
|
+
state: ConnectionState,
|
|
38
|
+
clock = new FakeClock(1_000),
|
|
39
|
+
motd: MotdProvider = EmptyMotdProvider,
|
|
40
|
+
): Ctx {
|
|
30
41
|
return buildCtx({
|
|
31
42
|
serverConfig,
|
|
32
43
|
clock,
|
|
33
44
|
ids: new SequentialIdFactory(),
|
|
34
|
-
motd
|
|
45
|
+
motd,
|
|
35
46
|
connection: state,
|
|
36
47
|
});
|
|
37
48
|
}
|
|
@@ -75,13 +86,11 @@ function expectedWelcome(nick: string, user: string, host: string | undefined):
|
|
|
75
86
|
);
|
|
76
87
|
return [
|
|
77
88
|
L(`${prefix} 001 ${nick} :Welcome to the ExampleNet IRC Network, ${hostmask}`),
|
|
78
|
-
L(`${prefix} 002 ${nick} :Your host is ${sv}, running version 0.
|
|
89
|
+
L(`${prefix} 002 ${nick} :Your host is ${sv}, running version 0.2.0`),
|
|
79
90
|
L(`${prefix} 003 ${nick} :This server was created in Phase 1`),
|
|
80
|
-
L(`${prefix} 004 ${nick} ${sv} 0.
|
|
91
|
+
L(`${prefix} 004 ${nick} ${sv} 0.2.0 iosw biklmnstp`),
|
|
81
92
|
...isupport,
|
|
82
|
-
L(`${prefix}
|
|
83
|
-
L(`${prefix} 372 ${nick} :- MOTD placeholder`),
|
|
84
|
-
L(`${prefix} 376 ${nick} :End of MOTD command`),
|
|
93
|
+
L(`${prefix} 422 ${nick} :MOTD File is missing`),
|
|
85
94
|
];
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -488,19 +497,22 @@ describe('passReducer', () => {
|
|
|
488
497
|
// ============================================================================
|
|
489
498
|
|
|
490
499
|
describe('buildWelcomeLines', () => {
|
|
491
|
-
it('produces the welcome block in the correct numeric order', () => {
|
|
500
|
+
it('produces the welcome block in the correct numeric order with MOTD content', () => {
|
|
492
501
|
const state = makeState();
|
|
493
502
|
state.nick = 'alice';
|
|
494
503
|
state.user = 'alice';
|
|
495
504
|
state.host = 'example.com';
|
|
496
|
-
const lines = buildWelcomeLines(
|
|
497
|
-
|
|
505
|
+
const lines = buildWelcomeLines(
|
|
506
|
+
state,
|
|
507
|
+
makeCtx(state, new FakeClock(1_000), new StaticMotdProvider(['line one', 'line two'])),
|
|
508
|
+
);
|
|
498
509
|
expect(lines.length).toBeGreaterThanOrEqual(8);
|
|
499
510
|
const codes = lines.map((l) => l.text.split(' ')[1]);
|
|
500
|
-
// First four numerics in fixed order; 005 (>=1);
|
|
511
|
+
// First four numerics in fixed order; 005 (>=1); MOTD block last.
|
|
501
512
|
expect(codes.slice(0, 4)).toEqual(['001', '002', '003', '004']);
|
|
502
513
|
expect(codes[4]).toBe('005');
|
|
503
|
-
expect(codes.at(-
|
|
514
|
+
expect(codes.at(-4)).toBe('375');
|
|
515
|
+
expect(codes.at(-3)).toBe('372');
|
|
504
516
|
expect(codes.at(-2)).toBe('372');
|
|
505
517
|
expect(codes.at(-1)).toBe('376');
|
|
506
518
|
});
|
|
@@ -556,6 +568,89 @@ describe('buildWelcomeLines', () => {
|
|
|
556
568
|
});
|
|
557
569
|
});
|
|
558
570
|
|
|
571
|
+
// ============================================================================
|
|
572
|
+
// buildWelcomeLines — MOTD driven from MotdProvider
|
|
573
|
+
// ============================================================================
|
|
574
|
+
|
|
575
|
+
describe('buildWelcomeLines — MOTD content', () => {
|
|
576
|
+
it('emits one 372 per MOTD line sourced from the MotdProvider', () => {
|
|
577
|
+
const state = makeState();
|
|
578
|
+
state.nick = 'alice';
|
|
579
|
+
state.user = 'alice';
|
|
580
|
+
state.host = 'example.com';
|
|
581
|
+
const lines = buildWelcomeLines(
|
|
582
|
+
state,
|
|
583
|
+
makeCtx(state, new FakeClock(1_000), new StaticMotdProvider(['line A', 'line B'])),
|
|
584
|
+
);
|
|
585
|
+
const rpl372 = lines.filter((l) => l.text.split(' ')[1] === '372');
|
|
586
|
+
expect(rpl372).toHaveLength(2);
|
|
587
|
+
expect(rpl372[0]).toEqual(L(':irc.example.com 372 alice :- line A'));
|
|
588
|
+
expect(rpl372[1]).toEqual(L(':irc.example.com 372 alice :- line B'));
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('frames the MOTD content with 375 start and 376 end', () => {
|
|
592
|
+
const state = makeState();
|
|
593
|
+
state.nick = 'alice';
|
|
594
|
+
state.user = 'alice';
|
|
595
|
+
const lines = buildWelcomeLines(
|
|
596
|
+
state,
|
|
597
|
+
makeCtx(state, new FakeClock(1_000), new StaticMotdProvider(['hi'])),
|
|
598
|
+
);
|
|
599
|
+
expect(lines).toContainEqual(
|
|
600
|
+
L(':irc.example.com 375 alice :- irc.example.com Message of the day -'),
|
|
601
|
+
);
|
|
602
|
+
expect(lines).toContainEqual(L(':irc.example.com 372 alice :- hi'));
|
|
603
|
+
expect(lines).toContainEqual(L(':irc.example.com 376 alice :End of MOTD command'));
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('reads MOTD lines fresh from the provider on each call', () => {
|
|
607
|
+
const state = makeState();
|
|
608
|
+
state.nick = 'alice';
|
|
609
|
+
state.user = 'alice';
|
|
610
|
+
const provider = new StaticMotdProvider(['first']);
|
|
611
|
+
const ctx = makeCtx(state, new FakeClock(1_000), provider);
|
|
612
|
+
const first = buildWelcomeLines(state, ctx).filter((l) => l.text.split(' ')[1] === '372');
|
|
613
|
+
provider.setLines(['first', 'second']);
|
|
614
|
+
const second = buildWelcomeLines(state, ctx).filter((l) => l.text.split(' ')[1] === '372');
|
|
615
|
+
expect(first).toHaveLength(1);
|
|
616
|
+
expect(second).toHaveLength(2);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('splits over-long MOTD lines so every emitted line fits the 510-byte budget', () => {
|
|
620
|
+
const state = makeState();
|
|
621
|
+
state.nick = 'alice';
|
|
622
|
+
state.user = 'alice';
|
|
623
|
+
state.host = 'example.com';
|
|
624
|
+
const long = 'A'.repeat(2_000);
|
|
625
|
+
const lines = buildWelcomeLines(
|
|
626
|
+
state,
|
|
627
|
+
makeCtx(state, new FakeClock(1_000), new StaticMotdProvider([long])),
|
|
628
|
+
);
|
|
629
|
+
for (const line of lines) {
|
|
630
|
+
expect(line.text.length).toBeLessThanOrEqual(510);
|
|
631
|
+
}
|
|
632
|
+
// The single over-long line is hard-wrapped into many 372 chunks.
|
|
633
|
+
const rpl372 = lines.filter((l) => l.text.split(' ')[1] === '372');
|
|
634
|
+
expect(rpl372.length).toBeGreaterThan(1);
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
describe('buildWelcomeLines — empty MOTD', () => {
|
|
639
|
+
it('emits 422 ERR_NOMOTD instead of a placeholder when no MOTD is configured', () => {
|
|
640
|
+
const state = makeState();
|
|
641
|
+
state.nick = 'alice';
|
|
642
|
+
state.user = 'alice';
|
|
643
|
+
const lines = buildWelcomeLines(state, makeCtx(state));
|
|
644
|
+
const codes = lines.map((l) => l.text.split(' ')[1]);
|
|
645
|
+
expect(codes.at(-1)).toBe('422');
|
|
646
|
+
expect(lines).toContainEqual(L(':irc.example.com 422 alice :MOTD File is missing'));
|
|
647
|
+
// No MOTD framing numerics leak through on the empty path.
|
|
648
|
+
expect(codes).not.toContain('375');
|
|
649
|
+
expect(codes).not.toContain('372');
|
|
650
|
+
expect(codes).not.toContain('376');
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
559
654
|
// ============================================================================
|
|
560
655
|
// emitWelcomeIfReady
|
|
561
656
|
// ============================================================================
|
|
@@ -567,33 +662,298 @@ describe('emitWelcomeIfReady', () => {
|
|
|
567
662
|
state.user = 'alice';
|
|
568
663
|
const ctx = makeCtx(state);
|
|
569
664
|
const effect = emitWelcomeIfReady(state, ctx);
|
|
570
|
-
expect(effect).toEqual(Effect.send('c1', expectedWelcome('alice', 'alice', undefined)));
|
|
665
|
+
expect(effect).toEqual([Effect.send('c1', expectedWelcome('alice', 'alice', undefined))]);
|
|
571
666
|
expect(state.registration).toBe('registered');
|
|
572
667
|
});
|
|
573
668
|
|
|
574
|
-
it('returns
|
|
669
|
+
it('returns an empty array and does not transition when nick is missing', () => {
|
|
575
670
|
const state = makeState();
|
|
576
671
|
state.user = 'alice';
|
|
577
672
|
const ctx = makeCtx(state);
|
|
578
673
|
const effect = emitWelcomeIfReady(state, ctx);
|
|
579
|
-
expect(effect).
|
|
674
|
+
expect(effect).toEqual([]);
|
|
580
675
|
expect(state.registration).toBe('pre-registration');
|
|
581
676
|
});
|
|
582
677
|
|
|
583
|
-
it('returns
|
|
678
|
+
it('returns an empty array and does not transition when user is missing', () => {
|
|
584
679
|
const state = makeState();
|
|
585
680
|
state.nick = 'alice';
|
|
586
681
|
const ctx = makeCtx(state);
|
|
587
682
|
const effect = emitWelcomeIfReady(state, ctx);
|
|
588
|
-
expect(effect).
|
|
683
|
+
expect(effect).toEqual([]);
|
|
589
684
|
expect(state.registration).toBe('pre-registration');
|
|
590
685
|
});
|
|
591
686
|
|
|
592
|
-
it('returns
|
|
687
|
+
it('returns an empty array when already registered', () => {
|
|
593
688
|
const state = registeredState();
|
|
594
689
|
const ctx = makeCtx(state);
|
|
595
690
|
const effect = emitWelcomeIfReady(state, ctx);
|
|
596
|
-
expect(effect).
|
|
691
|
+
expect(effect).toEqual([]);
|
|
692
|
+
expect(state.registration).toBe('registered');
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// ============================================================================
|
|
697
|
+
// emitWelcomeIfReady — server-password gate
|
|
698
|
+
// ============================================================================
|
|
699
|
+
|
|
700
|
+
/** Builds a ctx whose serverConfig has a serverPassword set. */
|
|
701
|
+
function makeCtxWithPassword(state: ConnectionState, password: string): Ctx {
|
|
702
|
+
return buildCtx({
|
|
703
|
+
serverConfig: { ...serverConfig, serverPassword: password },
|
|
704
|
+
clock: new FakeClock(1_000),
|
|
705
|
+
ids: new SequentialIdFactory(),
|
|
706
|
+
motd: EmptyMotdProvider,
|
|
707
|
+
connection: state,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
describe('emitWelcomeIfReady — server-password gate', () => {
|
|
712
|
+
it('proceeds with registration when no serverPassword is configured', () => {
|
|
713
|
+
const state = makeState();
|
|
714
|
+
state.nick = 'alice';
|
|
715
|
+
state.user = 'alice';
|
|
716
|
+
const ctx = makeCtx(state);
|
|
717
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
718
|
+
expect(effects).toEqual([Effect.send('c1', expectedWelcome('alice', 'alice', undefined))]);
|
|
719
|
+
expect(state.registration).toBe('registered');
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('proceeds with registration when serverPassword is empty (disabled)', () => {
|
|
723
|
+
const state = makeState();
|
|
724
|
+
state.nick = 'alice';
|
|
725
|
+
state.user = 'alice';
|
|
726
|
+
const ctx = makeCtxWithPassword(state, '');
|
|
727
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
728
|
+
expect(state.registration).toBe('registered');
|
|
729
|
+
expect(effects).toEqual([Effect.send('c1', expectedWelcome('alice', 'alice', undefined))]);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('blocks registration with 464 + Disconnect when passAttempt is missing', () => {
|
|
733
|
+
const state = makeState();
|
|
734
|
+
state.nick = 'alice';
|
|
735
|
+
state.user = 'alice';
|
|
736
|
+
const ctx = makeCtxWithPassword(state, 's3cret');
|
|
737
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
738
|
+
expect(state.registration).not.toBe('registered');
|
|
739
|
+
expect(effects).toEqual<EffectType[]>([
|
|
740
|
+
Effect.send('c1', [L(':irc.example.com 464 alice :Password mismatch')]),
|
|
741
|
+
Effect.disconnect('c1', 'Bad Password'),
|
|
742
|
+
]);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('blocks registration with 464 + Disconnect when passAttempt is wrong', () => {
|
|
746
|
+
const state = makeState();
|
|
747
|
+
state.nick = 'alice';
|
|
748
|
+
state.user = 'alice';
|
|
749
|
+
state.passAttempt = 'wrong';
|
|
750
|
+
const ctx = makeCtxWithPassword(state, 's3cret');
|
|
751
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
752
|
+
expect(state.registration).not.toBe('registered');
|
|
753
|
+
expect(effects).toEqual<EffectType[]>([
|
|
754
|
+
Effect.send('c1', [L(':irc.example.com 464 alice :Password mismatch')]),
|
|
755
|
+
Effect.disconnect('c1', 'Bad Password'),
|
|
756
|
+
]);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('proceeds with registration when passAttempt matches serverPassword', () => {
|
|
760
|
+
const state = makeState();
|
|
761
|
+
state.nick = 'alice';
|
|
762
|
+
state.user = 'alice';
|
|
763
|
+
state.passAttempt = 's3cret';
|
|
764
|
+
const ctx = makeCtxWithPassword(state, 's3cret');
|
|
765
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
766
|
+
expect(state.registration).toBe('registered');
|
|
767
|
+
expect(effects).toEqual([Effect.send('c1', expectedWelcome('alice', 'alice', undefined))]);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('satisfies the gate when SASL has authenticated an account (no PASS needed)', () => {
|
|
771
|
+
const state = makeState();
|
|
772
|
+
state.nick = 'alice';
|
|
773
|
+
state.user = 'alice';
|
|
774
|
+
state.account = 'alice';
|
|
775
|
+
const ctx = makeCtxWithPassword(state, 's3cret');
|
|
776
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
777
|
+
expect(state.registration).toBe('registered');
|
|
778
|
+
expect(effects).toEqual([Effect.send('c1', expectedWelcome('alice', 'alice', undefined))]);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('does not register when nick is missing even if password is configured', () => {
|
|
782
|
+
const state = makeState();
|
|
783
|
+
state.user = 'alice';
|
|
784
|
+
const ctx = makeCtxWithPassword(state, 's3cret');
|
|
785
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
786
|
+
expect(effects).toEqual([]);
|
|
787
|
+
expect(state.registration).toBe('pre-registration');
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('does not register when capNegotiating is true even if password is configured', () => {
|
|
791
|
+
const state = makeState();
|
|
792
|
+
state.nick = 'alice';
|
|
793
|
+
state.user = 'alice';
|
|
794
|
+
state.capNegotiating = true;
|
|
795
|
+
const ctx = makeCtxWithPassword(state, 's3cret');
|
|
796
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
797
|
+
expect(effects).toEqual([]);
|
|
798
|
+
expect(state.registration).toBe('pre-registration');
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// userReducer / nickReducer — server-password gate integration
|
|
804
|
+
// ============================================================================
|
|
805
|
+
|
|
806
|
+
describe('userReducer — server-password gate integration', () => {
|
|
807
|
+
it('emits 464 + Disconnect on the completing USER when passAttempt is wrong', () => {
|
|
808
|
+
const state = makeState();
|
|
809
|
+
state.nick = 'alice';
|
|
810
|
+
state.registration = 'registering';
|
|
811
|
+
const ctx = makeCtxWithPassword(state, 's3cret');
|
|
812
|
+
const out = userReducer(
|
|
813
|
+
state,
|
|
814
|
+
{ command: 'USER', params: ['alice', '0', '*', 'Alice'], tags: {} },
|
|
815
|
+
ctx,
|
|
816
|
+
);
|
|
817
|
+
expect(state.registration).not.toBe('registered');
|
|
818
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
819
|
+
Effect.send('c1', [L(':irc.example.com 464 alice :Password mismatch')]),
|
|
820
|
+
Effect.disconnect('c1', 'Bad Password'),
|
|
821
|
+
]);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it('completes registration on the completing USER when passAttempt matches', () => {
|
|
825
|
+
const state = makeState();
|
|
826
|
+
state.nick = 'alice';
|
|
827
|
+
state.passAttempt = 's3cret';
|
|
828
|
+
state.registration = 'registering';
|
|
829
|
+
const ctx = makeCtxWithPassword(state, 's3cret');
|
|
830
|
+
const out = userReducer(
|
|
831
|
+
state,
|
|
832
|
+
{ command: 'USER', params: ['alice', '0', '*', 'Alice'], tags: {} },
|
|
833
|
+
ctx,
|
|
834
|
+
);
|
|
597
835
|
expect(state.registration).toBe('registered');
|
|
836
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
837
|
+
Effect.send('c1', expectedWelcome('alice', 'alice', undefined)),
|
|
838
|
+
]);
|
|
598
839
|
});
|
|
599
840
|
});
|
|
841
|
+
|
|
842
|
+
// ============================================================================
|
|
843
|
+
// emitWelcomeIfReady — hostmask cloaking integration
|
|
844
|
+
// ============================================================================
|
|
845
|
+
|
|
846
|
+
function makeCtxWithCloaking(
|
|
847
|
+
state: ConnectionState,
|
|
848
|
+
cloaking: { enabled: boolean; secret: string; cloakedSuffix?: string },
|
|
849
|
+
): Ctx {
|
|
850
|
+
return buildCtx({
|
|
851
|
+
serverConfig: { ...serverConfig, cloaking },
|
|
852
|
+
clock: new FakeClock(1_000),
|
|
853
|
+
ids: new SequentialIdFactory(),
|
|
854
|
+
motd: EmptyMotdProvider,
|
|
855
|
+
connection: state,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
describe('emitWelcomeIfReady — hostmask cloaking', () => {
|
|
860
|
+
it('leaves host untouched when cloaking is disabled', () => {
|
|
861
|
+
const state = makeState();
|
|
862
|
+
state.nick = 'alice';
|
|
863
|
+
state.user = 'alice';
|
|
864
|
+
state.host = '203.0.113.5';
|
|
865
|
+
const ctx = makeCtxWithCloaking(state, { enabled: false, secret: 's3cret' });
|
|
866
|
+
emitWelcomeIfReady(state, ctx);
|
|
867
|
+
expect(state.host).toBe('203.0.113.5');
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('replaces the real host with a deterministic cloak when enabled', () => {
|
|
871
|
+
const state = makeState();
|
|
872
|
+
state.nick = 'alice';
|
|
873
|
+
state.user = 'alice';
|
|
874
|
+
state.host = '203.0.113.5';
|
|
875
|
+
const ctx = makeCtxWithCloaking(state, { enabled: true, secret: 's3cret' });
|
|
876
|
+
emitWelcomeIfReady(state, ctx);
|
|
877
|
+
expect(state.host).toBe(
|
|
878
|
+
cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'ExampleNet'),
|
|
879
|
+
);
|
|
880
|
+
expect(state.host).not.toBe('203.0.113.5');
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('uses the configured cloakedSuffix when supplied', () => {
|
|
884
|
+
const state = makeState();
|
|
885
|
+
state.nick = 'alice';
|
|
886
|
+
state.user = 'alice';
|
|
887
|
+
state.host = '203.0.113.5';
|
|
888
|
+
const ctx = makeCtxWithCloaking(state, {
|
|
889
|
+
enabled: true,
|
|
890
|
+
secret: 's3cret',
|
|
891
|
+
cloakedSuffix: 'hidden.lan',
|
|
892
|
+
});
|
|
893
|
+
emitWelcomeIfReady(state, ctx);
|
|
894
|
+
expect(state.host).toBe(
|
|
895
|
+
cloakHost(
|
|
896
|
+
'203.0.113.5',
|
|
897
|
+
{ enabled: true, secret: 's3cret', cloakedSuffix: 'hidden.lan' },
|
|
898
|
+
'ExampleNet',
|
|
899
|
+
),
|
|
900
|
+
);
|
|
901
|
+
expect(state.host.endsWith('.hidden.lan')).toBe(true);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('cloaks the host before building the welcome hostmask so 001 shows the cloak', () => {
|
|
905
|
+
const state = makeState();
|
|
906
|
+
state.nick = 'alice';
|
|
907
|
+
state.user = 'alice';
|
|
908
|
+
state.host = '203.0.113.5';
|
|
909
|
+
const ctx = makeCtxWithCloaking(state, { enabled: true, secret: 's3cret' });
|
|
910
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
911
|
+
const expectedCloak = cloakHost(
|
|
912
|
+
'203.0.113.5',
|
|
913
|
+
{ enabled: true, secret: 's3cret' },
|
|
914
|
+
'ExampleNet',
|
|
915
|
+
);
|
|
916
|
+
const expectedHostmask = `alice!alice@${expectedCloak}`;
|
|
917
|
+
expect(effects).toEqual([
|
|
918
|
+
Effect.send('c1', expectedWelcomeWithHost('alice', expectedHostmask)),
|
|
919
|
+
]);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('does not cloak when host is already absent (nothing to cloak)', () => {
|
|
923
|
+
const state = makeState();
|
|
924
|
+
state.nick = 'alice';
|
|
925
|
+
state.user = 'alice';
|
|
926
|
+
const ctx = makeCtxWithCloaking(state, { enabled: true, secret: 's3cret' });
|
|
927
|
+
emitWelcomeIfReady(state, ctx);
|
|
928
|
+
expect(state.host).toBeUndefined();
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
/** Builds the welcome block with an explicit cloaked host (used for cloak tests). */
|
|
933
|
+
function expectedWelcomeWithHost(nick: string, hostmask: string): RawLine[] {
|
|
934
|
+
const sv = 'irc.example.com';
|
|
935
|
+
const prefix = `:${sv}`;
|
|
936
|
+
const isupport = generateIsupport(
|
|
937
|
+
{
|
|
938
|
+
serverName: sv,
|
|
939
|
+
networkName: 'ExampleNet',
|
|
940
|
+
maxChannelsPerUser: 30,
|
|
941
|
+
maxTargetsPerCommand: 10,
|
|
942
|
+
maxListEntries: 50,
|
|
943
|
+
nickLen: 30,
|
|
944
|
+
channelLen: 50,
|
|
945
|
+
topicLen: 390,
|
|
946
|
+
quitMessage: 'Client Quit',
|
|
947
|
+
},
|
|
948
|
+
new Set<string>(),
|
|
949
|
+
nick,
|
|
950
|
+
);
|
|
951
|
+
return [
|
|
952
|
+
L(`${prefix} 001 ${nick} :Welcome to the ExampleNet IRC Network, ${hostmask}`),
|
|
953
|
+
L(`${prefix} 002 ${nick} :Your host is ${sv}, running version 0.2.0`),
|
|
954
|
+
L(`${prefix} 003 ${nick} :This server was created in Phase 1`),
|
|
955
|
+
L(`${prefix} 004 ${nick} ${sv} 0.2.0 iosw biklmnstp`),
|
|
956
|
+
...isupport,
|
|
957
|
+
L(`${prefix} 422 ${nick} :MOTD File is missing`),
|
|
958
|
+
];
|
|
959
|
+
}
|