serverless-ircd 0.7.0 → 0.9.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 +7 -3
- package/.gitmodules +1 -1
- package/CHANGELOG.md +518 -29
- package/README.md +221 -161
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +186 -18
- package/apps/aws-stack/tests/stack.test.ts +400 -56
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/container-server.ts +21 -1
- package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
- package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/worker.ts +4 -4
- package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
- package/apps/cf-worker/tests/smoke.test.ts +5 -5
- package/apps/cf-worker/wrangler.test.toml +6 -6
- package/apps/cf-worker/wrangler.toml +7 -5
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +8 -0
- package/apps/local-cli/src/main.ts +16 -0
- package/apps/local-cli/src/server.ts +95 -31
- package/apps/local-cli/tests/config-loader.test.ts +14 -0
- package/apps/local-cli/tests/config-resolution.test.ts +65 -0
- package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
- package/apps/local-cli/tests/rehash.test.ts +147 -0
- package/apps/local-cli/tests/server-helpers.test.ts +63 -0
- package/apps/local-cli/tests/tcp.test.ts +89 -0
- package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
- package/apps/web/landing/index.html +237 -16
- package/apps/web/package.json +3 -2
- package/apps/web/scripts/build.mjs +46 -16
- package/apps/web/src/build-env.ts +1 -1
- package/apps/web/src/config-schema.ts +6 -6
- package/apps/web/src/render-docs.ts +292 -0
- package/apps/web/tests/build-smoke.test.ts +43 -14
- package/apps/web/tests/config-schema.test.ts +1 -1
- package/apps/web/tests/landing-content.test.ts +103 -0
- package/apps/web/tests/render-docs.test.ts +198 -0
- package/docs/AWS-Adapter-Architecture.md +3 -2
- package/docs/AWS-Deployment.md +21 -8
- package/docs/Cloudflare-Deployment-Guide.md +22 -6
- package/docs/PlanExtensions.md +113 -3
- package/docs/Release-Process.md +23 -13
- package/docs/Services.md +578 -0
- package/docs/WebClientGuide.md +32 -31
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +20 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
- package/packages/aws-adapter/src/config-loader.ts +11 -0
- package/packages/aws-adapter/src/connection-counter.ts +89 -0
- package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
- package/packages/aws-adapter/src/handlers/connect.ts +55 -51
- package/packages/aws-adapter/src/handlers/default.ts +36 -4
- package/packages/aws-adapter/src/handlers/index.ts +15 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +25 -2
- package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
- package/packages/aws-adapter/src/index.ts +4 -0
- package/packages/aws-adapter/src/stats.ts +6 -1
- package/packages/aws-adapter/src/tables.ts +34 -4
- package/packages/aws-adapter/tests/aws-harness.ts +3 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
- package/packages/aws-adapter/tests/connect.test.ts +158 -32
- package/packages/aws-adapter/tests/connection-counter.test.ts +144 -0
- package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
- package/packages/aws-adapter/tests/global-setup.ts +28 -1
- package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
- package/packages/aws-adapter/tests/handlers.test.ts +105 -3
- package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
- package/packages/aws-adapter/tests/sweeper.test.ts +20 -0
- package/packages/aws-adapter/tests/tables.test.ts +6 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/config-loader.ts +11 -0
- package/packages/cf-adapter/src/connection-do.ts +130 -8
- package/packages/cf-adapter/src/d1-services-store.ts +703 -0
- package/packages/cf-adapter/src/env.ts +8 -0
- package/packages/cf-adapter/src/index.ts +5 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
- package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +130 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
- package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
- package/packages/cf-adapter/tests/serialize.test.ts +1 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
- package/packages/irc-core/src/commands/account-auth.ts +200 -0
- package/packages/irc-core/src/commands/chanserv.ts +1166 -0
- package/packages/irc-core/src/commands/hostserv.ts +522 -0
- package/packages/irc-core/src/commands/index.ts +13 -0
- package/packages/irc-core/src/commands/join.ts +171 -9
- package/packages/irc-core/src/commands/markread.ts +202 -0
- package/packages/irc-core/src/commands/memoserv.ts +319 -0
- package/packages/irc-core/src/commands/mode.ts +96 -4
- package/packages/irc-core/src/commands/nickserv.ts +402 -0
- package/packages/irc-core/src/commands/oper.ts +18 -1
- package/packages/irc-core/src/commands/operserv.ts +346 -0
- package/packages/irc-core/src/commands/pre-away.ts +3 -1
- package/packages/irc-core/src/commands/privmsg.ts +42 -0
- package/packages/irc-core/src/commands/read-marker.ts +8 -8
- package/packages/irc-core/src/commands/registration.ts +78 -12
- package/packages/irc-core/src/commands/sasl.ts +18 -49
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/tagmsg.ts +41 -6
- package/packages/irc-core/src/commands/topic.ts +60 -10
- package/packages/irc-core/src/config.ts +36 -5
- package/packages/irc-core/src/effects.ts +56 -1
- package/packages/irc-core/src/ports.ts +1653 -84
- package/packages/irc-core/src/protocol/numerics.ts +8 -0
- package/packages/irc-core/src/state/channel.ts +38 -1
- package/packages/irc-core/src/state/connection.ts +25 -1
- package/packages/irc-core/src/types.ts +48 -12
- package/packages/irc-core/tests/commands/chanserv.test.ts +2335 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +1006 -0
- package/packages/irc-core/tests/commands/join.test.ts +572 -1
- package/packages/irc-core/tests/commands/markread.test.ts +361 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
- package/packages/irc-core/tests/commands/mode.test.ts +381 -2
- package/packages/irc-core/tests/commands/nickserv.test.ts +990 -0
- package/packages/irc-core/tests/commands/oper.test.ts +13 -0
- package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
- package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
- package/packages/irc-core/tests/commands/registration.test.ts +1015 -20
- package/packages/irc-core/tests/commands/sasl.test.ts +229 -12
- package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
- package/packages/irc-core/tests/commands/topic.test.ts +94 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
- package/packages/irc-core/tests/config.test.ts +49 -5
- package/packages/irc-core/tests/effects.test.ts +19 -0
- package/packages/irc-core/tests/message-store.test.ts +63 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
- package/packages/irc-core/tests/services-store.test.ts +1289 -0
- package/packages/irc-core/tests/state/channel.test.ts +3 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +121 -16
- package/packages/irc-server/src/dispatch.ts +94 -7
- package/packages/irc-server/src/routing.ts +19 -0
- package/packages/irc-server/tests/actor.test.ts +988 -15
- package/packages/irc-server/tests/dispatch.test.ts +270 -2
- package/packages/irc-server/tests/routing.test.ts +6 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +37 -8
- package/packages/irc-test-support/src/index.ts +1 -0
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +51 -0
- package/packages/irc-test-support/vitest.config.ts +6 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/load-test/src/client.ts +13 -13
- package/tools/load-test/tests/client.test.ts +258 -2
- package/tools/load-test/tests/config.test.ts +39 -0
- package/tools/load-test/tests/harness.test.ts +21 -0
- package/tools/load-test/tests/metrics.test.ts +7 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
- package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +53 -2
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
- package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for the NickServ services pseudo-client.
|
|
3
|
+
*
|
|
4
|
+
* NickServ is invoked via `PRIVMSG NickServ :<command>`. The actor routes
|
|
5
|
+
* any `PRIVMSG` whose target matches {@link NICKSERV_NICK}
|
|
6
|
+
* (case-insensitive) to {@link nickservReducer} instead of the normal
|
|
7
|
+
* user-target reducer.
|
|
8
|
+
*
|
|
9
|
+
* Supported subcommands:
|
|
10
|
+
* - `REGISTER <password> <email>` — registers the current nick against
|
|
11
|
+
* the bound {@link ServicesStore}. After registration, SASL PLAIN with
|
|
12
|
+
* `<nick>:<password>` succeeds (the SASL reducer falls back to the
|
|
13
|
+
* services store). Does NOT auto-identify.
|
|
14
|
+
* - `IDENTIFY [nick] <password>` — verifies the credentials and, on
|
|
15
|
+
* success, records the account on the connection, sets read-only user
|
|
16
|
+
* mode `+r`, fans an `ACCOUNT` line out to `account-notify` peers, and
|
|
17
|
+
* delivers queued MemoServ memos.
|
|
18
|
+
* - `DROP [nick]` — removes the nick registration. Requires the caller
|
|
19
|
+
* to be identified first; clears `+r` and the account, fans `ACCOUNT *`
|
|
20
|
+
* out to peers.
|
|
21
|
+
* - `INFO [nick]` — queries the registration record.
|
|
22
|
+
* - `SET ENFORCE none|ghost|kill` — sets the per-account nick-enforcement
|
|
23
|
+
* policy. Requires identification. The policy drives
|
|
24
|
+
* {@link enforceRegisteredNick}, which the NICK path consults when a
|
|
25
|
+
* client takes a registered nick they are not identified as.
|
|
26
|
+
*
|
|
27
|
+
* The reducer is a pure `Reducer<ConnectionState>`: all side effects are
|
|
28
|
+
* emitted as {@link Effect}s. The services store is synchronous (mirrors
|
|
29
|
+
* the {@link AccountStore} / {@link MessageStore} pattern).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { DEFAULT_NICK_ENFORCE_GRACE_MS } from '../config.js';
|
|
33
|
+
import { Effect } from '../effects.js';
|
|
34
|
+
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
35
|
+
import type { NickEnforcePolicy, ServicesStore } from '../ports.js';
|
|
36
|
+
import type { ConnectionState } from '../state/connection.js';
|
|
37
|
+
import type { Ctx, Reducer } from '../types.js';
|
|
38
|
+
import { emitAccountNotify } from './account-notify.js';
|
|
39
|
+
import { applyAssignedVhost } from './hostserv.js';
|
|
40
|
+
import { deliverUnreadMemos } from './memoserv.js';
|
|
41
|
+
|
|
42
|
+
/** Canonical NickServ pseudo-client nick. */
|
|
43
|
+
export const NICKSERV_NICK = 'NickServ';
|
|
44
|
+
|
|
45
|
+
/** Services hostmask fragment used by every services pseudo-client. */
|
|
46
|
+
const SERVICES_HOST = 'services';
|
|
47
|
+
|
|
48
|
+
/** Policies accepted by `SET ENFORCE` (lowercased input forms). */
|
|
49
|
+
const ENFORCE_POLICIES: ReadonlySet<NickEnforcePolicy> = new Set(['none', 'ghost', 'kill']);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reducer for `PRIVMSG NickServ :<subcommand> <args…>`.
|
|
53
|
+
* Authority: the invoking connection's {@link ConnectionState}.
|
|
54
|
+
*/
|
|
55
|
+
export const nickservReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
|
|
56
|
+
state.lastSeen = ctx.clock.now();
|
|
57
|
+
|
|
58
|
+
const effects: EffectType[] = [];
|
|
59
|
+
|
|
60
|
+
if (ctx.services === undefined) {
|
|
61
|
+
effects.push(notice(state, 'Services are not available on this server.'));
|
|
62
|
+
return { state, effects };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const body = msg.params[1] ?? '';
|
|
66
|
+
const tokens = body.split(/\s+/u).filter((t) => t.length > 0);
|
|
67
|
+
if (tokens.length === 0) {
|
|
68
|
+
effects.push(helpNotice(state));
|
|
69
|
+
return { state, effects };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const sub = tokens[0]?.toUpperCase();
|
|
73
|
+
const args = tokens.slice(1);
|
|
74
|
+
|
|
75
|
+
switch (sub) {
|
|
76
|
+
case 'REGISTER':
|
|
77
|
+
return handleRegister(state, args, ctx, effects);
|
|
78
|
+
case 'ID':
|
|
79
|
+
case 'IDENTIFY':
|
|
80
|
+
return handleIdentify(state, args, ctx, effects);
|
|
81
|
+
case 'DROP':
|
|
82
|
+
return handleDrop(state, args, ctx, effects);
|
|
83
|
+
case 'INFO':
|
|
84
|
+
return handleInfo(state, args, ctx, effects);
|
|
85
|
+
case 'SET':
|
|
86
|
+
return handleSet(state, args, ctx, effects);
|
|
87
|
+
default:
|
|
88
|
+
effects.push(unknownNotice(state));
|
|
89
|
+
return { state, effects };
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// REGISTER
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
function handleRegister(
|
|
98
|
+
state: ConnectionState,
|
|
99
|
+
args: string[],
|
|
100
|
+
ctx: Ctx,
|
|
101
|
+
effects: EffectType[],
|
|
102
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
103
|
+
const password = args[0];
|
|
104
|
+
const email = args[1];
|
|
105
|
+
const nick = state.nick;
|
|
106
|
+
if (nick === undefined || password === undefined || email === undefined) {
|
|
107
|
+
effects.push(notice(state, 'Syntax: REGISTER <password> <email>'));
|
|
108
|
+
return { state, effects };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const services = ctx.services as ServicesStore;
|
|
112
|
+
const result = services.registerNick(nick, password, email);
|
|
113
|
+
if (result.ok) {
|
|
114
|
+
effects.push(notice(state, `Nickname ${nick} is now registered.`));
|
|
115
|
+
} else {
|
|
116
|
+
effects.push(notice(state, `Nickname ${nick} is already registered.`));
|
|
117
|
+
}
|
|
118
|
+
return { state, effects };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// IDENTIFY
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
function handleIdentify(
|
|
126
|
+
state: ConnectionState,
|
|
127
|
+
args: string[],
|
|
128
|
+
ctx: Ctx,
|
|
129
|
+
effects: EffectType[],
|
|
130
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
131
|
+
const services = ctx.services as ServicesStore;
|
|
132
|
+
|
|
133
|
+
if (state.account !== undefined) {
|
|
134
|
+
effects.push(notice(state, 'You are already identified.'));
|
|
135
|
+
return { state, effects };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// IDENTIFY <password> → identify current nick
|
|
139
|
+
// IDENTIFY <nick> <password> → identify named nick
|
|
140
|
+
let nick: string | undefined;
|
|
141
|
+
let password: string | undefined;
|
|
142
|
+
if (args.length >= 2) {
|
|
143
|
+
nick = args[0];
|
|
144
|
+
password = args[1];
|
|
145
|
+
} else {
|
|
146
|
+
nick = state.nick;
|
|
147
|
+
password = args[0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (nick === undefined || password === undefined) {
|
|
151
|
+
effects.push(notice(state, 'Syntax: IDENTIFY [nick] <password>'));
|
|
152
|
+
return { state, effects };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!services.isRegisteredNick(nick)) {
|
|
156
|
+
effects.push(notice(state, `Nick ${nick} is not registered.`));
|
|
157
|
+
return { state, effects };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const result = services.verifyNick(nick, password);
|
|
161
|
+
if (!result.ok) {
|
|
162
|
+
effects.push(notice(state, 'Invalid password.'));
|
|
163
|
+
return { state, effects };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
applyIdentify(state, result.account, services, effects);
|
|
167
|
+
return { state, effects };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Applies a successful identify: records the account, sets `+r`, re-applies any
|
|
172
|
+
* assigned HostServ vhost (so a reconnecting user does not have to re-run
|
|
173
|
+
* `HostServ ON`), emits the confirmation NOTICE, fans `ACCOUNT` out to peers,
|
|
174
|
+
* and delivers queued memos.
|
|
175
|
+
*/
|
|
176
|
+
function applyIdentify(
|
|
177
|
+
state: ConnectionState,
|
|
178
|
+
account: string,
|
|
179
|
+
services: ServicesStore,
|
|
180
|
+
effects: EffectType[],
|
|
181
|
+
): void {
|
|
182
|
+
state.account = account;
|
|
183
|
+
state.userModes.registered = true;
|
|
184
|
+
// Re-apply an assigned HostServ vhost. At identify time the connection is
|
|
185
|
+
// usually pre-JOIN so the CHGHOST fanout is empty, but a post-JOIN IDENTIFY
|
|
186
|
+
// fans CHGHOST out to the user's current `chghost`-capable peers.
|
|
187
|
+
applyAssignedVhost(state, services, account, effects);
|
|
188
|
+
effects.push(notice(state, `You are now identified for nick ${account}.`));
|
|
189
|
+
effects.push(...emitAccountNotify({ conn: state, account }));
|
|
190
|
+
deliverUnreadMemos(state, services, account, effects);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// DROP
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
function handleDrop(
|
|
198
|
+
state: ConnectionState,
|
|
199
|
+
_args: string[],
|
|
200
|
+
ctx: Ctx,
|
|
201
|
+
effects: EffectType[],
|
|
202
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
203
|
+
const services = ctx.services as ServicesStore;
|
|
204
|
+
|
|
205
|
+
if (state.account === undefined) {
|
|
206
|
+
effects.push(notice(state, 'You must identify before dropping a nick.'));
|
|
207
|
+
return { state, effects };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const nick = state.nick;
|
|
211
|
+
if (nick === undefined) {
|
|
212
|
+
effects.push(notice(state, 'You must identify before dropping a nick.'));
|
|
213
|
+
return { state, effects };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
services.dropNick(nick);
|
|
217
|
+
// Clear the identification state and announce the logout to peers.
|
|
218
|
+
// biome-ignore lint/performance/noDelete: exactOptionalPropertyTypes forbids `= undefined`.
|
|
219
|
+
delete state.account;
|
|
220
|
+
state.userModes.registered = false;
|
|
221
|
+
effects.push(notice(state, `Nickname ${nick} has been dropped.`));
|
|
222
|
+
effects.push(...emitAccountNotify({ conn: state }));
|
|
223
|
+
return { state, effects };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// INFO
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
function handleInfo(
|
|
231
|
+
state: ConnectionState,
|
|
232
|
+
args: string[],
|
|
233
|
+
ctx: Ctx,
|
|
234
|
+
effects: EffectType[],
|
|
235
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
236
|
+
const services = ctx.services as ServicesStore;
|
|
237
|
+
const target = args[0] ?? state.nick;
|
|
238
|
+
if (target === undefined) {
|
|
239
|
+
effects.push(notice(state, 'Syntax: INFO [nick]'));
|
|
240
|
+
return { state, effects };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const rec = services.getNick(target);
|
|
244
|
+
if (rec === undefined) {
|
|
245
|
+
effects.push(notice(state, `Nick ${target} is not registered.`));
|
|
246
|
+
return { state, effects };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
effects.push(notice(state, `Nick: ${rec.nick}`));
|
|
250
|
+
effects.push(notice(state, `Account: ${rec.account}`));
|
|
251
|
+
// Email is private: only the account owner or an oper may see it. Mirrors
|
|
252
|
+
// the `state.account === rec.account` gate used by SET ENFORCE / the
|
|
253
|
+
// enforcement hook, extended to opers.
|
|
254
|
+
if (state.account === rec.account || state.userModes.oper) {
|
|
255
|
+
effects.push(notice(state, `Email: ${rec.email}`));
|
|
256
|
+
}
|
|
257
|
+
return { state, effects };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Nick-enforcement hook (consulted by the NICK path)
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Warning NOTICE text sent to every unidentified client using a registered
|
|
266
|
+
* nick, regardless of the account's enforcement policy.
|
|
267
|
+
*/
|
|
268
|
+
const ENFORCE_WARNING_TEXT =
|
|
269
|
+
'This nickname is registered and protected. If it is yours, IDENTIFY; otherwise choose another.';
|
|
270
|
+
|
|
271
|
+
/** Canonical disconnect reason the actor applies on enforcement. */
|
|
272
|
+
export const ENFORCE_NICK_REASON = 'Nick enforced by NickServ';
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Returns the effects to emit when a client has just taken `state.nick`:
|
|
276
|
+
* if the nick is registered and the connection is NOT identified as the
|
|
277
|
+
* owning account, the {@link NickEnforcePolicy} on the registration drives
|
|
278
|
+
* the output:
|
|
279
|
+
*
|
|
280
|
+
* - `none` — warning NOTICE only.
|
|
281
|
+
* - `kill` — warning NOTICE + `EnforceNick(kill, graceMs=0)`.
|
|
282
|
+
* - `ghost` — warning NOTICE + `EnforceNick(ghost, graceMs=<config>)`.
|
|
283
|
+
*
|
|
284
|
+
* Returns `[]` when services are unbound, the connection has no nick, the
|
|
285
|
+
* nick is not registered, or the connection is identified as the nick's
|
|
286
|
+
* account owner. Called by the registration path on welcome-completion and
|
|
287
|
+
* by the post-registration `NICK` change.
|
|
288
|
+
*/
|
|
289
|
+
export function enforceRegisteredNick(state: ConnectionState, ctx: Ctx): EffectType[] {
|
|
290
|
+
const services = ctx.services;
|
|
291
|
+
if (services === undefined) return [];
|
|
292
|
+
const nick = state.nick;
|
|
293
|
+
if (nick === undefined) return [];
|
|
294
|
+
|
|
295
|
+
const rec = services.getNick(nick);
|
|
296
|
+
if (rec === undefined) return [];
|
|
297
|
+
// Identified as the owning account → the use is legitimate.
|
|
298
|
+
if (state.account !== undefined && state.account === rec.account) return [];
|
|
299
|
+
|
|
300
|
+
const effects: EffectType[] = [notice(state, ENFORCE_WARNING_TEXT)];
|
|
301
|
+
|
|
302
|
+
if (rec.enforce === 'none') return effects;
|
|
303
|
+
|
|
304
|
+
if (rec.enforce === 'kill') {
|
|
305
|
+
effects.push(Effect.enforceNick(state.id, nick, 'kill', 0));
|
|
306
|
+
return effects;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ghost: stamp the configured grace so dispatch can schedule the
|
|
310
|
+
// disconnect. `0` (config-disabled grace) collapses to immediate.
|
|
311
|
+
const graceMs = ctx.serverConfig.nickEnforceGraceMs ?? DEFAULT_NICK_ENFORCE_GRACE_MS;
|
|
312
|
+
effects.push(Effect.enforceNick(state.id, nick, 'ghost', graceMs));
|
|
313
|
+
return effects;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// SET
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Handles `SET <subkey> <args…>`. Currently only `ENFORCE none|ghost|kill`
|
|
322
|
+
* is supported; unknown subkeys fall back to the ENFORCE syntax notice.
|
|
323
|
+
*
|
|
324
|
+
* Requires the caller to be identified as the account bound to the current
|
|
325
|
+
* nick: enforcement is a per-account policy, so an unidentified client
|
|
326
|
+
* (or one identified as a different account) cannot mutate it.
|
|
327
|
+
*/
|
|
328
|
+
function handleSet(
|
|
329
|
+
state: ConnectionState,
|
|
330
|
+
args: string[],
|
|
331
|
+
ctx: Ctx,
|
|
332
|
+
effects: EffectType[],
|
|
333
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
334
|
+
const services = ctx.services as ServicesStore;
|
|
335
|
+
const subkey = args[0]?.toUpperCase();
|
|
336
|
+
if (subkey !== 'ENFORCE') {
|
|
337
|
+
effects.push(enforceSyntaxNotice(state));
|
|
338
|
+
return { state, effects };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const policyRaw = args[1]?.toLowerCase();
|
|
342
|
+
if (policyRaw === undefined || !ENFORCE_POLICIES.has(policyRaw as NickEnforcePolicy)) {
|
|
343
|
+
effects.push(enforceSyntaxNotice(state));
|
|
344
|
+
return { state, effects };
|
|
345
|
+
}
|
|
346
|
+
const policy = policyRaw as NickEnforcePolicy;
|
|
347
|
+
|
|
348
|
+
const nick = state.nick;
|
|
349
|
+
if (nick === undefined || state.account === undefined) {
|
|
350
|
+
effects.push(notice(state, 'You must identify before changing enforcement.'));
|
|
351
|
+
return { state, effects };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const rec = services.getNick(nick);
|
|
355
|
+
if (rec === undefined) {
|
|
356
|
+
effects.push(notice(state, `Nick ${nick} is not registered.`));
|
|
357
|
+
return { state, effects };
|
|
358
|
+
}
|
|
359
|
+
// The caller must be identified as the account that owns this nick.
|
|
360
|
+
if (state.account !== rec.account) {
|
|
361
|
+
effects.push(notice(state, 'You must identify before changing enforcement.'));
|
|
362
|
+
return { state, effects };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
services.setNickEnforce(nick, policy);
|
|
366
|
+
effects.push(notice(state, `Enforcement for nick ${nick} is now set to ${policy}.`));
|
|
367
|
+
return { state, effects };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Builds the `SET ENFORCE` syntax NOTICE. */
|
|
371
|
+
function enforceSyntaxNotice(state: ConnectionState): EffectType {
|
|
372
|
+
return notice(state, 'Syntax: SET ENFORCE none|ghost|kill');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// Notice helpers
|
|
377
|
+
// ============================================================================
|
|
378
|
+
|
|
379
|
+
/** Builds a `:NickServ!NickServ@services NOTICE <nick> :<text>` line. */
|
|
380
|
+
function notice(state: ConnectionState, text: string): EffectType {
|
|
381
|
+
return Effect.send(state.id, [servicesNotice(NICKSERV_NICK, state, text)]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Builds a help/unknown `:NickServ!NickServ@services NOTICE <nick> :…` line. */
|
|
385
|
+
function helpNotice(state: ConnectionState): EffectType {
|
|
386
|
+
return notice(state, 'Available commands: REGISTER, IDENTIFY, DROP, INFO, SET');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function unknownNotice(state: ConnectionState): EffectType {
|
|
390
|
+
return notice(state, 'Unknown command. Available: REGISTER, IDENTIFY, DROP, INFO, SET');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Formats a services NOTICE line. The source hostmask is
|
|
395
|
+
* `<svc>!<svc>@services` regardless of the deployment's serverName so the
|
|
396
|
+
* pseudo-clients are stable across deployments and easy to identify in
|
|
397
|
+
* client UIs.
|
|
398
|
+
*/
|
|
399
|
+
function servicesNotice(svc: string, state: ConnectionState, text: string): RawLine {
|
|
400
|
+
const nick = state.nick ?? '*';
|
|
401
|
+
return { text: `:${svc}!${svc}@${SERVICES_HOST} NOTICE ${nick} :${text}` };
|
|
402
|
+
}
|
|
@@ -27,15 +27,19 @@
|
|
|
27
27
|
* - Missing `<name>` or `<password>` → `461 ERR_NEEDMOREPARAMS`.
|
|
28
28
|
* - Already an operator → graceful idempotent no-op that re-confirms with
|
|
29
29
|
* `381` (no re-authentication required; the connection is already privy).
|
|
30
|
+
* No `MODE` echo is emitted because the user mode did not change.
|
|
30
31
|
* - No operator credentials configured (absent or empty) →
|
|
31
32
|
* `491 ERR_NOOPERHOST`.
|
|
32
|
-
* - Matching credential pair → grant oper, emit `381
|
|
33
|
+
* - Matching credential pair → grant oper, emit `381`, then emit a
|
|
34
|
+
* `:<hostmask> MODE <nick> +o` notification to the connection so the
|
|
35
|
+
* client learns its user mode changed (mirrors the user `MODE` echo).
|
|
33
36
|
* - Wrong password / unknown name → `464 ERR_PASSWDMISMATCH`.
|
|
34
37
|
*/
|
|
35
38
|
|
|
36
39
|
import { Effect } from '../effects.js';
|
|
37
40
|
import type { RawLine } from '../effects.js';
|
|
38
41
|
import { Numerics } from '../protocol/numerics.js';
|
|
42
|
+
import { hostmaskOf } from '../state/connection.js';
|
|
39
43
|
import type { ConnectionState } from '../state/connection.js';
|
|
40
44
|
import type { OperCred } from '../types.js';
|
|
41
45
|
import type { Ctx, Reducer } from '../types.js';
|
|
@@ -122,6 +126,7 @@ export const operReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
|
|
|
122
126
|
Effect.send(ctx.connId, [
|
|
123
127
|
numericLine(ctx, Numerics.RPL_YOUREOPER, YOUREOPER_TRAILING_TEXT),
|
|
124
128
|
]),
|
|
129
|
+
Effect.send(ctx.connId, [modeEchoLine(state)]),
|
|
125
130
|
],
|
|
126
131
|
};
|
|
127
132
|
}
|
|
@@ -149,3 +154,15 @@ function numericLine(ctx: Ctx, code: number, trailing: string, middle?: string):
|
|
|
149
154
|
parts.push(`:${trailing}`);
|
|
150
155
|
return { text: parts.join(' ') };
|
|
151
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Builds the `:<hostmask> MODE <nick> +o` echo emitted to the connection
|
|
160
|
+
* when oper is granted. Mirrors the user `MODE` echo format so clients can
|
|
161
|
+
* treat oper-up as a regular user-mode change. The hostmask falls back to
|
|
162
|
+
* the nick (then `?`) to match the rest of the codebase's source formatting.
|
|
163
|
+
*/
|
|
164
|
+
function modeEchoLine(state: ConnectionState): RawLine {
|
|
165
|
+
const nick = state.nick ?? '*';
|
|
166
|
+
const source = hostmaskOf(state) ?? state.nick ?? '?';
|
|
167
|
+
return { text: `:${source} MODE ${nick} +o` };
|
|
168
|
+
}
|