serverless-ircd 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +34 -2
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/CHANGELOG.md +435 -0
- package/README.md +206 -27
- package/apps/aws-stack/README.md +37 -3
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +106 -13
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +49 -3
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.toml +5 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +138 -28
- package/apps/local-cli/tests/e2e.test.ts +113 -29
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +262 -0
- package/biome.json +1 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +15 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +69 -0
- package/packages/aws-adapter/src/handlers/connect.ts +36 -5
- package/packages/aws-adapter/src/handlers/default.ts +66 -5
- package/packages/aws-adapter/src/handlers/index.ts +41 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +18 -0
- package/packages/aws-adapter/src/index.ts +2 -0
- package/packages/aws-adapter/src/serialize.ts +40 -2
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +140 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +100 -4
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +238 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +62 -8
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/cf-adapter/package.json +6 -1
- package/packages/cf-adapter/src/cf-runtime.ts +66 -1
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/connection-do.ts +185 -54
- package/packages/cf-adapter/src/env.ts +25 -6
- package/packages/cf-adapter/src/index.ts +2 -0
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +1 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +4 -4
- package/packages/cf-adapter/tests/cf-runtime.test.ts +307 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +119 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +108 -4
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +7 -7
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/cf-adapter/wrangler.test.toml +7 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +38 -4
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +42 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/isupport.ts +59 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +12 -9
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +72 -10
- package/packages/irc-core/src/effects.ts +41 -1
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +568 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +57 -11
- package/packages/irc-core/src/protocol/outbound.ts +36 -4
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +120 -1
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +129 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +90 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +699 -19
- package/packages/irc-server/src/dispatch.ts +109 -16
- package/packages/irc-server/src/routing.ts +3 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +10 -7
- package/packages/irc-server/tests/actor.test.ts +2523 -42
- package/packages/irc-server/tests/dispatch.test.ts +300 -2
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +1 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +9 -1
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/package.json +6 -1
- package/tools/seed-cf-accounts.ts +4 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { wallopsReducer } from '../../src/commands/wallops';
|
|
3
|
+
import { Effect } from '../../src/effects';
|
|
4
|
+
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
|
+
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
6
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
7
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
8
|
+
|
|
9
|
+
const serverConfig: ServerConfig = {
|
|
10
|
+
serverName: 'irc.example.com',
|
|
11
|
+
networkName: 'ExampleNet',
|
|
12
|
+
maxChannelsPerUser: 30,
|
|
13
|
+
maxTargetsPerCommand: 10,
|
|
14
|
+
maxListEntries: 50,
|
|
15
|
+
nickLen: 30,
|
|
16
|
+
channelLen: 50,
|
|
17
|
+
topicLen: 390,
|
|
18
|
+
quitMessage: 'Client Quit',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function makeCtx(conn: ConnectionState, clock = new FakeClock(5_000)): Ctx {
|
|
22
|
+
return buildCtx({
|
|
23
|
+
serverConfig,
|
|
24
|
+
clock,
|
|
25
|
+
ids: new SequentialIdFactory(),
|
|
26
|
+
motd: EmptyMotdProvider,
|
|
27
|
+
connection: conn,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeOper(id = 'c1', nick = 'alice'): ConnectionState {
|
|
32
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
33
|
+
s.nick = nick;
|
|
34
|
+
s.user = nick;
|
|
35
|
+
s.host = 'example.com';
|
|
36
|
+
s.realname = 'Alice';
|
|
37
|
+
s.registration = 'registered';
|
|
38
|
+
s.userModes.oper = true;
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const L = (text: string): RawLine => ({ text });
|
|
43
|
+
|
|
44
|
+
const wallops = (message?: string) =>
|
|
45
|
+
({
|
|
46
|
+
command: 'WALLOPS',
|
|
47
|
+
params: message === undefined ? [] : [message],
|
|
48
|
+
tags: {},
|
|
49
|
+
}) as const;
|
|
50
|
+
|
|
51
|
+
describe('wallopsReducer — oper happy path', () => {
|
|
52
|
+
it('emits a global BroadcastWallops effect carrying the oper source prefix', () => {
|
|
53
|
+
const oper = makeOper();
|
|
54
|
+
const ctx = makeCtx(oper);
|
|
55
|
+
|
|
56
|
+
const out = wallopsReducer(oper, wallops('hello'), ctx);
|
|
57
|
+
|
|
58
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
59
|
+
Effect.broadcastWallops([L(':alice!alice@example.com WALLOPS :hello')], 'c1'),
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('passes the message text through verbatim (multi-word)', () => {
|
|
64
|
+
const oper = makeOper();
|
|
65
|
+
const ctx = makeCtx(oper);
|
|
66
|
+
|
|
67
|
+
const out = wallopsReducer(oper, wallops('restart in 5 minutes'), ctx);
|
|
68
|
+
|
|
69
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
70
|
+
Effect.broadcastWallops([L(':alice!alice@example.com WALLOPS :restart in 5 minutes')], 'c1'),
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('defaults except to the sender (skip-self, NOTICE/PRIVMSG semantics)', () => {
|
|
75
|
+
const oper = makeOper();
|
|
76
|
+
const ctx = makeCtx(oper);
|
|
77
|
+
|
|
78
|
+
const out = wallopsReducer(oper, wallops('hi'), ctx);
|
|
79
|
+
|
|
80
|
+
expect(out.effects[0]?.tag).toBe('BroadcastWallops');
|
|
81
|
+
// except is the oper's own connection id, so the runtime never echoes
|
|
82
|
+
// the wallops back to its originator.
|
|
83
|
+
expect((out.effects[0] as { except?: string }).except).toBe('c1');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('updates the oper lastSeen to ctx.clock.now()', () => {
|
|
87
|
+
const oper = makeOper();
|
|
88
|
+
expect(oper.lastSeen).toBe(0);
|
|
89
|
+
const ctx = makeCtx(oper);
|
|
90
|
+
|
|
91
|
+
wallopsReducer(oper, wallops('hi'), ctx);
|
|
92
|
+
|
|
93
|
+
expect(oper.lastSeen).toBe(5_000);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns the same state reference unchanged', () => {
|
|
97
|
+
const oper = makeOper();
|
|
98
|
+
const ctx = makeCtx(oper);
|
|
99
|
+
|
|
100
|
+
const out = wallopsReducer(oper, wallops('hi'), ctx);
|
|
101
|
+
|
|
102
|
+
expect(out.state).toBe(oper);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('falls back to the bare nick when the hostmask is incomplete (no user/host)', () => {
|
|
106
|
+
const oper = createConnection({ id: 'c1', connectedSince: 0 });
|
|
107
|
+
oper.nick = 'alice';
|
|
108
|
+
oper.registration = 'registered';
|
|
109
|
+
oper.userModes.oper = true;
|
|
110
|
+
const ctx = makeCtx(oper);
|
|
111
|
+
|
|
112
|
+
const out = wallopsReducer(oper, wallops('hi'), ctx);
|
|
113
|
+
|
|
114
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
115
|
+
Effect.broadcastWallops([L(':alice WALLOPS :hi')], 'c1'),
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('falls back to "*" when the oper has neither nick nor hostmask (defensive)', () => {
|
|
120
|
+
const oper = createConnection({ id: 'c1', connectedSince: 0 });
|
|
121
|
+
oper.registration = 'registered';
|
|
122
|
+
oper.userModes.oper = true;
|
|
123
|
+
const ctx = makeCtx(oper);
|
|
124
|
+
|
|
125
|
+
const out = wallopsReducer(oper, wallops('hi'), ctx);
|
|
126
|
+
|
|
127
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
128
|
+
Effect.broadcastWallops([L(':* WALLOPS :hi')], 'c1'),
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('wallopsReducer — non-oper (481 ERR_NOPRIVILEGES)', () => {
|
|
134
|
+
it('emits 481 when the sender is not an oper', () => {
|
|
135
|
+
const oper = makeOper();
|
|
136
|
+
oper.userModes.oper = false;
|
|
137
|
+
const ctx = makeCtx(oper);
|
|
138
|
+
|
|
139
|
+
const out = wallopsReducer(oper, wallops('hi'), ctx);
|
|
140
|
+
|
|
141
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
142
|
+
Effect.send('c1', [
|
|
143
|
+
L(":irc.example.com 481 alice :Permission Denied - You're not an IRC operator"),
|
|
144
|
+
]),
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('does not emit a BroadcastWallops when the sender is not an oper', () => {
|
|
149
|
+
const oper = makeOper();
|
|
150
|
+
oper.userModes.oper = false;
|
|
151
|
+
const ctx = makeCtx(oper);
|
|
152
|
+
|
|
153
|
+
const out = wallopsReducer(oper, wallops('hi'), ctx);
|
|
154
|
+
|
|
155
|
+
expect(out.effects.some((e) => e.tag === 'BroadcastWallops')).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('gates before param validation: a non-oper with no message still gets 481', () => {
|
|
159
|
+
const oper = makeOper();
|
|
160
|
+
oper.userModes.oper = false;
|
|
161
|
+
const ctx = makeCtx(oper);
|
|
162
|
+
|
|
163
|
+
const out = wallopsReducer(oper, wallops(), ctx);
|
|
164
|
+
|
|
165
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
166
|
+
Effect.send('c1', [
|
|
167
|
+
L(":irc.example.com 481 alice :Permission Denied - You're not an IRC operator"),
|
|
168
|
+
]),
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('uses "*" as the nick in 481 when the connection has no nick', () => {
|
|
173
|
+
const oper = createConnection({ id: 'c1', connectedSince: 0 });
|
|
174
|
+
oper.userModes.oper = false;
|
|
175
|
+
const ctx = makeCtx(oper);
|
|
176
|
+
|
|
177
|
+
const out = wallopsReducer(oper, wallops('hi'), ctx);
|
|
178
|
+
|
|
179
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
180
|
+
Effect.send('c1', [
|
|
181
|
+
L(":irc.example.com 481 * :Permission Denied - You're not an IRC operator"),
|
|
182
|
+
]),
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('wallopsReducer — empty message (412 ERR_NOTEXTTOSEND)', () => {
|
|
188
|
+
it('emits 412 when no message parameter is supplied', () => {
|
|
189
|
+
const oper = makeOper();
|
|
190
|
+
const ctx = makeCtx(oper);
|
|
191
|
+
|
|
192
|
+
const out = wallopsReducer(oper, wallops(), ctx);
|
|
193
|
+
|
|
194
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
195
|
+
Effect.send('c1', [L(':irc.example.com 412 alice :No text to send')]),
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('emits 412 when the message is the empty string', () => {
|
|
200
|
+
const oper = makeOper();
|
|
201
|
+
const ctx = makeCtx(oper);
|
|
202
|
+
|
|
203
|
+
const out = wallopsReducer(oper, wallops(''), ctx);
|
|
204
|
+
|
|
205
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
206
|
+
Effect.send('c1', [L(':irc.example.com 412 alice :No text to send')]),
|
|
207
|
+
]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('does not broadcast when the message is empty', () => {
|
|
211
|
+
const oper = makeOper();
|
|
212
|
+
const ctx = makeCtx(oper);
|
|
213
|
+
|
|
214
|
+
const out = wallopsReducer(oper, wallops(''), ctx);
|
|
215
|
+
|
|
216
|
+
expect(out.effects.some((e) => e.tag === 'BroadcastWallops')).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('uses "*" as the nick in 412 when the connection has no nick', () => {
|
|
220
|
+
const oper = createConnection({ id: 'c1', connectedSince: 0 });
|
|
221
|
+
oper.registration = 'registered';
|
|
222
|
+
oper.userModes.oper = true;
|
|
223
|
+
const ctx = makeCtx(oper);
|
|
224
|
+
|
|
225
|
+
const out = wallopsReducer(oper, wallops(''), ctx);
|
|
226
|
+
|
|
227
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
228
|
+
Effect.send('c1', [L(':irc.example.com 412 * :No text to send')]),
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -166,6 +166,40 @@ describe('whoisReducer — happy path', () => {
|
|
|
166
166
|
expect(send.lines.some((l) => l.text.endsWith(':is an IRC operator'))).toBe(true);
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
|
+
|
|
170
|
+
it('emits 276 RPL_WHOISSECURE when the target connected over TLS (user mode S)', () => {
|
|
171
|
+
const requester = makeConn();
|
|
172
|
+
const ctx = makeCtx(requester);
|
|
173
|
+
const target = makeTarget('c2', 'bob');
|
|
174
|
+
target.userModes.tls = true;
|
|
175
|
+
|
|
176
|
+
const out = whoisReducer(target, [], { command: 'WHOIS', params: ['bob'] }, ctx);
|
|
177
|
+
|
|
178
|
+
const send = out.effects[0];
|
|
179
|
+
expect(send?.tag).toBe('Send');
|
|
180
|
+
if (send?.tag === 'Send') {
|
|
181
|
+
expect(send.lines.some((l) => l.text.includes(' 276 '))).toBe(true);
|
|
182
|
+
expect(
|
|
183
|
+
send.lines.some(
|
|
184
|
+
(l) => l.text === ':irc.example.com 276 alice bob :is using a secure connection',
|
|
185
|
+
),
|
|
186
|
+
).toBe(true);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('does not emit 276 when the target is not on a secure connection', () => {
|
|
191
|
+
const requester = makeConn();
|
|
192
|
+
const ctx = makeCtx(requester);
|
|
193
|
+
const target = makeTarget('c2', 'bob');
|
|
194
|
+
// tls left at its default (false).
|
|
195
|
+
|
|
196
|
+
const out = whoisReducer(target, [], { command: 'WHOIS', params: ['bob'] }, ctx);
|
|
197
|
+
|
|
198
|
+
const send = out.effects[0];
|
|
199
|
+
if (send?.tag === 'Send') {
|
|
200
|
+
expect(send.lines.some((l) => l.text.includes(' 276 '))).toBe(false);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
169
203
|
});
|
|
170
204
|
|
|
171
205
|
// ============================================================================
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_CREATED_TEXT,
|
|
4
|
-
DEFAULT_SERVER_CONFIG,
|
|
5
4
|
DEFAULT_SERVER_VERSION,
|
|
6
5
|
ServerConfigSchema,
|
|
7
6
|
formatCreatedAt,
|
|
@@ -567,22 +566,34 @@ describe('ServerConfigSchema — failure diagnostics', () => {
|
|
|
567
566
|
});
|
|
568
567
|
|
|
569
568
|
// ============================================================================
|
|
570
|
-
// ServerConfigSchema —
|
|
569
|
+
// ServerConfigSchema — no placeholder serverName baseline
|
|
571
570
|
// ============================================================================
|
|
572
571
|
|
|
573
|
-
describe('ServerConfigSchema —
|
|
574
|
-
it('
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
572
|
+
describe('ServerConfigSchema — required serverName', () => {
|
|
573
|
+
it('rejects an empty input with a serverName error (no placeholder fallback)', () => {
|
|
574
|
+
try {
|
|
575
|
+
parseServerConfig({});
|
|
576
|
+
expect.fail('expected parseServerConfig({}) to throw');
|
|
577
|
+
} catch (err) {
|
|
578
|
+
expect(err).toBeInstanceOf(Error);
|
|
579
|
+
const msg = (err as Error).message;
|
|
580
|
+
expect(msg).toMatch(/invalid server config/iu);
|
|
581
|
+
expect(msg).toMatch(/serverName/u);
|
|
582
|
+
}
|
|
581
583
|
});
|
|
582
584
|
|
|
583
|
-
it('
|
|
584
|
-
|
|
585
|
-
|
|
585
|
+
it('rejects an empty-string serverName', () => {
|
|
586
|
+
expect(() => parseServerConfig({ serverName: '', networkName: 'n' })).toThrowError(
|
|
587
|
+
/serverName/u,
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('accepts a minimal {serverName, networkName} input', () => {
|
|
592
|
+
const cfg = parseServerConfig({ serverName: 'irc.test', networkName: 'TestNet' });
|
|
593
|
+
expect(cfg.serverName).toBe('irc.test');
|
|
594
|
+
expect(cfg.networkName).toBe('TestNet');
|
|
595
|
+
expect(cfg.channelPrefixes.length).toBeGreaterThan(0);
|
|
596
|
+
expect(cfg.maxClients).toBeGreaterThan(0);
|
|
586
597
|
});
|
|
587
598
|
});
|
|
588
599
|
|
|
@@ -813,3 +824,82 @@ describe('ServerConfigSchema — perIpConnectionRate', () => {
|
|
|
813
824
|
).toThrowError(/perIpConnectionRate/u);
|
|
814
825
|
});
|
|
815
826
|
});
|
|
827
|
+
|
|
828
|
+
// ============================================================================
|
|
829
|
+
// ServerConfigSchema — sts (Strict Transport Security)
|
|
830
|
+
// ============================================================================
|
|
831
|
+
|
|
832
|
+
describe('ServerConfigSchema — sts', () => {
|
|
833
|
+
it('defaults sts to undefined when omitted (STS disabled)', () => {
|
|
834
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
835
|
+
expect(cfg.sts).toBeUndefined();
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('accepts a valid sts config with duration + port', () => {
|
|
839
|
+
const cfg = parseServerConfig({
|
|
840
|
+
serverName: 's',
|
|
841
|
+
networkName: 'n',
|
|
842
|
+
sts: { duration: 600, port: 6697 },
|
|
843
|
+
});
|
|
844
|
+
expect(cfg.sts).toEqual({ duration: 600, port: 6697 });
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('accepts an optional preload directive', () => {
|
|
848
|
+
const cfg = parseServerConfig({
|
|
849
|
+
serverName: 's',
|
|
850
|
+
networkName: 'n',
|
|
851
|
+
sts: { duration: 600, port: 6697, preload: true },
|
|
852
|
+
});
|
|
853
|
+
expect(cfg.sts?.preload).toBe(true);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('rejects an sts config missing duration', () => {
|
|
857
|
+
expect(() =>
|
|
858
|
+
parseServerConfig({
|
|
859
|
+
serverName: 's',
|
|
860
|
+
networkName: 'n',
|
|
861
|
+
sts: { port: 6697 },
|
|
862
|
+
}),
|
|
863
|
+
).toThrowError(/duration/u);
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('rejects an sts config missing port', () => {
|
|
867
|
+
expect(() =>
|
|
868
|
+
parseServerConfig({
|
|
869
|
+
serverName: 's',
|
|
870
|
+
networkName: 'n',
|
|
871
|
+
sts: { duration: 600 },
|
|
872
|
+
}),
|
|
873
|
+
).toThrowError(/port/u);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('rejects a zero duration', () => {
|
|
877
|
+
expect(() =>
|
|
878
|
+
parseServerConfig({
|
|
879
|
+
serverName: 's',
|
|
880
|
+
networkName: 'n',
|
|
881
|
+
sts: { duration: 0, port: 6697 },
|
|
882
|
+
}),
|
|
883
|
+
).toThrowError(/duration/u);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('rejects a zero port', () => {
|
|
887
|
+
expect(() =>
|
|
888
|
+
parseServerConfig({
|
|
889
|
+
serverName: 's',
|
|
890
|
+
networkName: 'n',
|
|
891
|
+
sts: { duration: 600, port: 0 },
|
|
892
|
+
}),
|
|
893
|
+
).toThrowError(/port/u);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('rejects a non-object sts value', () => {
|
|
897
|
+
expect(() =>
|
|
898
|
+
parseServerConfig({
|
|
899
|
+
serverName: 's',
|
|
900
|
+
networkName: 'n',
|
|
901
|
+
sts: 'secure',
|
|
902
|
+
}),
|
|
903
|
+
).toThrowError(/sts/u);
|
|
904
|
+
});
|
|
905
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression guards for the formally-dropped S2S and obsolete RFC 2812 verbs.
|
|
3
|
+
*
|
|
4
|
+
* - S2S drop: `CONNECT` / `SQUIT` / `LINKS` are server-to-server commands.
|
|
5
|
+
* S2S is an explicit PLAN non-goal (`PLAN.md` §1), so the three verbs are
|
|
6
|
+
* formally dropped (they still return `421 ERR_UNKNOWNCOMMAND` — that is
|
|
7
|
+
* the correct response for an unimplemented command). Their reserved
|
|
8
|
+
* numerics (`RPL_LINKS`, `RPL_ENDOFLINKS`, `ERR_CANTKILLSERVER`) are removed
|
|
9
|
+
* here.
|
|
10
|
+
*
|
|
11
|
+
* - Obsolete-verb drop: `SERVICE` / `SUMMON` / `USERS` are RFC 2812 verbs
|
|
12
|
+
* that were never widely implemented and are effectively dead. They are
|
|
13
|
+
* formally dropped (also still `421`). Their reserved numerics
|
|
14
|
+
* (`RPL_YOURESERVICE`, `ERR_NOSUCHSERVICE`, `ERR_SUMMONDISABLED`,
|
|
15
|
+
* `ERR_USERSDISABLED`) are removed here.
|
|
16
|
+
*
|
|
17
|
+
* Because the drop is "remove the numeric, keep the 421", there is no runtime
|
|
18
|
+
* behaviour to assert — these are source-level / registry-level regression
|
|
19
|
+
* guards that prevent the numerics (and the "deferred verb" framing) from
|
|
20
|
+
* creeping back in.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, expect, it } from 'vitest';
|
|
24
|
+
import { Numerics, numericToName } from '../src/protocol/numerics';
|
|
25
|
+
import numericsSource from '../src/protocol/numerics.ts?raw';
|
|
26
|
+
|
|
27
|
+
describe('dropped S2S verbs (CONNECT / SQUIT / LINKS)', () => {
|
|
28
|
+
// S2S is a PLAN non-goal; the three verbs are formally dropped. Their
|
|
29
|
+
// reserved numerics are removed from the registry.
|
|
30
|
+
const droppedS2SNumerics = ['RPL_LINKS', 'RPL_ENDOFLINKS', 'ERR_CANTKILLSERVER'] as const;
|
|
31
|
+
|
|
32
|
+
it.each(droppedS2SNumerics)('numerics.ts source no longer defines %s', (name) => {
|
|
33
|
+
expect(numericsSource).not.toContain(`${name}:`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.each(droppedS2SNumerics)('Numerics runtime object has no %s key', (name) => {
|
|
37
|
+
expect((Numerics as Record<string, unknown>)[name]).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('numericToName no longer resolves the dropped S2S codes (364, 365, 483)', () => {
|
|
41
|
+
expect(numericToName.get(364)).toBeUndefined();
|
|
42
|
+
expect(numericToName.get(365)).toBeUndefined();
|
|
43
|
+
expect(numericToName.get(483)).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('numerics.ts source documents the S2S drop', () => {
|
|
47
|
+
// The drop must be explained where the numerics used to live, so the
|
|
48
|
+
// absence is not mistaken for a gap.
|
|
49
|
+
expect(numericsSource).toMatch(/S2S/);
|
|
50
|
+
expect(numericsSource).toMatch(/PLAN non-goal/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('ERR_NOSUCHSERVER (402) is retained (used by LUSERS/STATS/TRACE)', () => {
|
|
54
|
+
// 402 stays because the implemented query verbs use it for remote-server
|
|
55
|
+
// gating.
|
|
56
|
+
expect(Numerics.ERR_NOSUCHSERVER).toBe(402);
|
|
57
|
+
expect(numericToName.get(402)).toBe('ERR_NOSUCHSERVER');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('dropped obsolete RFC 2812 verbs (SERVICE / SUMMON / USERS)', () => {
|
|
62
|
+
// RFC 2812 obsolete verbs, never widely implemented. Formally dropped;
|
|
63
|
+
// their reserved numerics are removed from the registry.
|
|
64
|
+
const droppedObsoleteNumerics = [
|
|
65
|
+
'RPL_YOURESERVICE',
|
|
66
|
+
'ERR_NOSUCHSERVICE',
|
|
67
|
+
'ERR_SUMMONDISABLED',
|
|
68
|
+
'ERR_USERSDISABLED',
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
it.each(droppedObsoleteNumerics)('numerics.ts source no longer defines %s', (name) => {
|
|
72
|
+
expect(numericsSource).not.toContain(`${name}:`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it.each(droppedObsoleteNumerics)('Numerics runtime object has no %s key', (name) => {
|
|
76
|
+
expect((Numerics as Record<string, unknown>)[name]).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('numericToName no longer resolves the dropped obsolete codes (383, 408, 445, 446)', () => {
|
|
80
|
+
expect(numericToName.get(383)).toBeUndefined();
|
|
81
|
+
expect(numericToName.get(408)).toBeUndefined();
|
|
82
|
+
expect(numericToName.get(445)).toBeUndefined();
|
|
83
|
+
expect(numericToName.get(446)).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('numerics.ts source documents the obsolete-verb drop', () => {
|
|
87
|
+
expect(numericsSource).toMatch(/RFC 2812/);
|
|
88
|
+
expect(numericsSource).toMatch(/obsolete/);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -82,6 +82,18 @@ describe('Effect constructors', () => {
|
|
|
82
82
|
notFoundLines: [{ text: '401' }],
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
it('BroadcastWallops carries lines and optional except', () => {
|
|
87
|
+
expect(Effect.broadcastWallops([line(':a WALLOPS :hi')])).toEqual<EffectType>({
|
|
88
|
+
tag: 'BroadcastWallops',
|
|
89
|
+
lines: [{ text: ':a WALLOPS :hi' }],
|
|
90
|
+
});
|
|
91
|
+
expect(Effect.broadcastWallops([line(':a WALLOPS :hi')], 'c1')).toEqual<EffectType>({
|
|
92
|
+
tag: 'BroadcastWallops',
|
|
93
|
+
lines: [{ text: ':a WALLOPS :hi' }],
|
|
94
|
+
except: 'c1',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
85
97
|
});
|
|
86
98
|
|
|
87
99
|
describe('Effect tag set', () => {
|
|
@@ -95,6 +107,7 @@ describe('Effect tag set', () => {
|
|
|
95
107
|
'ReleaseNick',
|
|
96
108
|
'ApplyChannelDelta',
|
|
97
109
|
'SendToNick',
|
|
110
|
+
'BroadcastWallops',
|
|
98
111
|
];
|
|
99
112
|
|
|
100
113
|
it.each(expectedTags)('exposes a constructor for the %s tag', (tag) => {
|
|
@@ -107,6 +120,7 @@ describe('Effect tag set', () => {
|
|
|
107
120
|
ReleaseNick: Effect.releaseNick('n'),
|
|
108
121
|
ApplyChannelDelta: Effect.applyChannelDelta('#c', { memberships: [] }),
|
|
109
122
|
SendToNick: Effect.sendToNick('n', 'c', []),
|
|
123
|
+
BroadcastWallops: Effect.broadcastWallops([]),
|
|
110
124
|
};
|
|
111
125
|
expect(constructors[tag].tag).toBe(tag);
|
|
112
126
|
});
|