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,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for the HostServ services pseudo-client.
|
|
3
|
+
*
|
|
4
|
+
* HostServ is invoked via `PRIVMSG HostServ :<command>`. The actor routes
|
|
5
|
+
* any `PRIVMSG` whose target matches {@link HOSTSERV_NICK}
|
|
6
|
+
* (case-insensitive) to {@link hostservReducer} instead of the normal
|
|
7
|
+
* user-target reducer.
|
|
8
|
+
*
|
|
9
|
+
* Supported subcommands:
|
|
10
|
+
* - `ON` — applies the per-account vhost (set via REQUEST or oper SET)
|
|
11
|
+
* to the connection's visible host, persists the prior host so OFF can
|
|
12
|
+
* restore it, and emits `CHGHOST` (IRCv3 `chghost`) to every peer the
|
|
13
|
+
* user shares a channel with. Requires the connection to be identified.
|
|
14
|
+
* - `OFF` — restores the real (pre-vhost) host and emits `CHGHOST` so
|
|
15
|
+
* peers see the revert. Requires identification.
|
|
16
|
+
* - `REQUEST <vhost>` — proposes a vhost for the account. Behaviour
|
|
17
|
+
* depends on {@link ServerConfig.hostservAutoApproveVhosts}: when the
|
|
18
|
+
* knob is on (the default) the vhost is recorded against the account
|
|
19
|
+
* immediately (legacy auto-approve); when the knob is off the request
|
|
20
|
+
* is queued for oper review via {@link ServicesStore.requestVhost}
|
|
21
|
+
* and the requester is told it is pending. Requires identification.
|
|
22
|
+
* - `SET <account> <vhost>` — oper-only. Records (or, with the literal
|
|
23
|
+
* `*` argument, clears) the vhost for `account` directly without
|
|
24
|
+
* going through REQUEST. The target account must be a registered nick.
|
|
25
|
+
* - `APPROVE <account>` (alias `ACTIVATE`) — oper-only. Activates the
|
|
26
|
+
* pending vhost request recorded via queue-mode REQUEST, replying to
|
|
27
|
+
* the oper and best-effort NOTICE-ing the requester when online.
|
|
28
|
+
* - `REJECT <account>` — oper-only. Drops the pending vhost request,
|
|
29
|
+
* replying to the oper and best-effort NOTICE-ing the requester when
|
|
30
|
+
* online.
|
|
31
|
+
* - `LIST` — oper-only. Enumerates every pending vhost request.
|
|
32
|
+
*
|
|
33
|
+
* The reducer is a pure `Reducer<ConnectionState>`: all side effects are
|
|
34
|
+
* emitted as {@link Effect}s. The CHGHOST fanout reuses the
|
|
35
|
+
* {@link emitChghost} broadcast helper so the wire shape stays consistent
|
|
36
|
+
* with SETNAME and any other user/host mutation.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { Effect } from '../effects.js';
|
|
40
|
+
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
41
|
+
import type { ServicesStore } from '../ports.js';
|
|
42
|
+
import type { ConnectionState } from '../state/connection.js';
|
|
43
|
+
import type { Ctx, Reducer } from '../types.js';
|
|
44
|
+
import { emitChghost } from './chghost.js';
|
|
45
|
+
|
|
46
|
+
/** Canonical HostServ pseudo-client nick. */
|
|
47
|
+
export const HOSTSERV_NICK = 'HostServ';
|
|
48
|
+
|
|
49
|
+
/** Services hostmask fragment used by every services pseudo-client. */
|
|
50
|
+
const SERVICES_HOST = 'services';
|
|
51
|
+
|
|
52
|
+
/** Sentinel value used by `SET <account> *` to clear a recorded vhost. */
|
|
53
|
+
const CLEAR_VHOST_SENTINEL = '*';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Grammar for a HostServ vhost. Permits the dotted-label form used by
|
|
57
|
+
* mainstream IRC services (each label bounded by alphanumerics, hyphens
|
|
58
|
+
* permitted in the interior). Rejects whitespace, IRC protocol metachars,
|
|
59
|
+
* and empty labels. Capped at 63 characters per label (DNS label limit)
|
|
60
|
+
* and a total of 253 characters (DNS hostname limit).
|
|
61
|
+
*/
|
|
62
|
+
const VHOST_RE =
|
|
63
|
+
/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/u;
|
|
64
|
+
|
|
65
|
+
/** Returns true iff `vhost` is a syntactically valid HostServ vhost. */
|
|
66
|
+
function isValidVhost(vhost: string): boolean {
|
|
67
|
+
return VHOST_RE.test(vhost);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reducer for `PRIVMSG HostServ :<subcommand> <args…>`.
|
|
72
|
+
* Authority: the invoking connection's {@link ConnectionState}.
|
|
73
|
+
*/
|
|
74
|
+
export const hostservReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
|
|
75
|
+
state.lastSeen = ctx.clock.now();
|
|
76
|
+
|
|
77
|
+
const effects: EffectType[] = [];
|
|
78
|
+
|
|
79
|
+
if (ctx.services === undefined) {
|
|
80
|
+
effects.push(notice(state, 'Services are not available on this server.'));
|
|
81
|
+
return { state, effects };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const body = msg.params[1] ?? '';
|
|
85
|
+
const tokens = body.split(/\s+/u).filter((t) => t.length > 0);
|
|
86
|
+
if (tokens.length === 0) {
|
|
87
|
+
effects.push(helpNotice(state));
|
|
88
|
+
return { state, effects };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sub = tokens[0]?.toUpperCase();
|
|
92
|
+
const args = tokens.slice(1);
|
|
93
|
+
|
|
94
|
+
switch (sub) {
|
|
95
|
+
case 'ON':
|
|
96
|
+
return handleOn(state, ctx, effects);
|
|
97
|
+
case 'OFF':
|
|
98
|
+
return handleOff(state, ctx, effects);
|
|
99
|
+
case 'REQUEST':
|
|
100
|
+
return handleRequest(state, args, ctx, effects);
|
|
101
|
+
case 'SET':
|
|
102
|
+
return handleSet(state, args, ctx, effects);
|
|
103
|
+
case 'APPROVE':
|
|
104
|
+
case 'ACTIVATE':
|
|
105
|
+
return handleApprove(state, args, ctx, effects);
|
|
106
|
+
case 'REJECT':
|
|
107
|
+
return handleReject(state, args, ctx, effects);
|
|
108
|
+
case 'LIST':
|
|
109
|
+
return handleList(state, ctx, effects);
|
|
110
|
+
default:
|
|
111
|
+
effects.push(unknownNotice(state));
|
|
112
|
+
return { state, effects };
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// ON
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handles `ON`. Applies the connection's per-account vhost (looked up from
|
|
122
|
+
* the bound {@link ServicesStore}) to {@link ConnectionState.host}, records
|
|
123
|
+
* the prior host on {@link ConnectionState.preVhostHost} so OFF can restore
|
|
124
|
+
* it, and broadcasts `CHGHOST` to every shared-channel peer.
|
|
125
|
+
*/
|
|
126
|
+
function handleOn(
|
|
127
|
+
state: ConnectionState,
|
|
128
|
+
ctx: Ctx,
|
|
129
|
+
effects: EffectType[],
|
|
130
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
131
|
+
if (state.account === undefined) {
|
|
132
|
+
effects.push(notice(state, 'You must identify before enabling a vhost.'));
|
|
133
|
+
return { state, effects };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (state.vhostActive === true) {
|
|
137
|
+
effects.push(notice(state, 'Your vhost is already active.'));
|
|
138
|
+
return { state, effects };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const services = ctx.services as ServicesStore;
|
|
142
|
+
const vhost = services.getVhost(state.account);
|
|
143
|
+
if (vhost === undefined) {
|
|
144
|
+
effects.push(notice(state, 'No vhost is assigned to your account.'));
|
|
145
|
+
return { state, effects };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
applyVhost(state, vhost, effects);
|
|
149
|
+
effects.push(notice(state, 'Your vhost is now active.'));
|
|
150
|
+
return { state, effects };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// OFF
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handles `OFF`. Restores the real (pre-vhost) host from
|
|
159
|
+
* {@link ConnectionState.preVhostHost} and broadcasts `CHGHOST`.
|
|
160
|
+
*/
|
|
161
|
+
function handleOff(
|
|
162
|
+
state: ConnectionState,
|
|
163
|
+
_ctx: Ctx,
|
|
164
|
+
effects: EffectType[],
|
|
165
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
166
|
+
if (state.account === undefined) {
|
|
167
|
+
effects.push(notice(state, 'You must identify before changing your vhost.'));
|
|
168
|
+
return { state, effects };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (state.vhostActive !== true) {
|
|
172
|
+
effects.push(notice(state, 'No vhost is currently active.'));
|
|
173
|
+
return { state, effects };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
restoreRealHost(state, effects);
|
|
177
|
+
effects.push(notice(state, 'Your vhost has been disabled.'));
|
|
178
|
+
return { state, effects };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// REQUEST
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Handles `REQUEST <vhost>`. Behaviour is selected by
|
|
187
|
+
* {@link ServerConfig.hostservAutoApproveVhosts}:
|
|
188
|
+
* - `true` (auto-approve) — the vhost is recorded against the account
|
|
189
|
+
* immediately (the path that shipped before the oper queue landed). The
|
|
190
|
+
* user is notified that the vhost has been requested and approved.
|
|
191
|
+
* - `false` or omitted (the default — queue mode) — the request is
|
|
192
|
+
* queued for oper review via {@link ServicesStore.requestVhost}; the
|
|
193
|
+
* vhost is NOT activated and no `CHGHOST` is emitted. The user is
|
|
194
|
+
* notified that the request is pending oper approval.
|
|
195
|
+
*
|
|
196
|
+
* Identified-only — otherwise a guest could plant a vhost (or a pending
|
|
197
|
+
* request) against a registered account they do not own.
|
|
198
|
+
*/
|
|
199
|
+
function handleRequest(
|
|
200
|
+
state: ConnectionState,
|
|
201
|
+
args: string[],
|
|
202
|
+
ctx: Ctx,
|
|
203
|
+
effects: EffectType[],
|
|
204
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
205
|
+
if (state.account === undefined) {
|
|
206
|
+
effects.push(notice(state, 'You must identify before requesting a vhost.'));
|
|
207
|
+
return { state, effects };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const vhost = args[0];
|
|
211
|
+
if (vhost === undefined) {
|
|
212
|
+
effects.push(notice(state, 'Syntax: REQUEST <vhost>'));
|
|
213
|
+
return { state, effects };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!isValidVhost(vhost)) {
|
|
217
|
+
effects.push(notice(state, `Invalid vhost: ${vhost}`));
|
|
218
|
+
return { state, effects };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const services = ctx.services as ServicesStore;
|
|
222
|
+
if (ctx.serverConfig.hostservAutoApproveVhosts !== true) {
|
|
223
|
+
services.requestVhost(state.account, vhost);
|
|
224
|
+
effects.push(notice(state, `Vhost ${vhost} is pending oper approval.`));
|
|
225
|
+
return { state, effects };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
services.setVhost(state.account, vhost);
|
|
229
|
+
effects.push(notice(state, `Vhost ${vhost} has been requested and approved.`));
|
|
230
|
+
return { state, effects };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// SET (oper-only)
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Handles `SET <account> <vhost>` (oper-only). Records the vhost against
|
|
239
|
+
* the named account directly, bypassing REQUEST. The literal `*` clears
|
|
240
|
+
* the recorded vhost. The target account must already be a registered
|
|
241
|
+
* nick; SET does not auto-register.
|
|
242
|
+
*/
|
|
243
|
+
function handleSet(
|
|
244
|
+
state: ConnectionState,
|
|
245
|
+
args: string[],
|
|
246
|
+
ctx: Ctx,
|
|
247
|
+
effects: EffectType[],
|
|
248
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
249
|
+
if (!state.userModes.oper) {
|
|
250
|
+
effects.push(notice(state, 'You must be an oper to use SET.'));
|
|
251
|
+
return { state, effects };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const account = args[0];
|
|
255
|
+
const vhost = args[1];
|
|
256
|
+
if (account === undefined || vhost === undefined) {
|
|
257
|
+
effects.push(notice(state, 'Syntax: SET <account> <vhost>'));
|
|
258
|
+
return { state, effects };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const services = ctx.services as ServicesStore;
|
|
262
|
+
if (!services.isRegisteredNick(account)) {
|
|
263
|
+
effects.push(notice(state, `Nick ${account} is not registered.`));
|
|
264
|
+
return { state, effects };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (vhost === CLEAR_VHOST_SENTINEL) {
|
|
268
|
+
services.setVhost(account, '');
|
|
269
|
+
effects.push(notice(state, `Vhost for ${account} has been cleared.`));
|
|
270
|
+
return { state, effects };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!isValidVhost(vhost)) {
|
|
274
|
+
effects.push(notice(state, `Invalid vhost: ${vhost}`));
|
|
275
|
+
return { state, effects };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
services.setVhost(account, vhost);
|
|
279
|
+
effects.push(notice(state, `Vhost for ${account} is now set to ${vhost}.`));
|
|
280
|
+
return { state, effects };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// APPROVE / ACTIVATE (oper-only)
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Handles `APPROVE <account>` (alias `ACTIVATE`). Activates the pending
|
|
289
|
+
* vhost request recorded via queue-mode REQUEST: removes the pending
|
|
290
|
+
* entry, calls {@link ServicesStore.setVhost} to activate the vhost, and
|
|
291
|
+
* replies to the oper. A best-effort `SendToNick` NOTICE is emitted to
|
|
292
|
+
* the requester when they are online — delivery is owned by the actor
|
|
293
|
+
* layer (the reducer cannot mutate another connection's host directly).
|
|
294
|
+
* The requester is told to run `HostServ ON` to apply the vhost to their
|
|
295
|
+
* active connection.
|
|
296
|
+
*/
|
|
297
|
+
function handleApprove(
|
|
298
|
+
state: ConnectionState,
|
|
299
|
+
args: string[],
|
|
300
|
+
ctx: Ctx,
|
|
301
|
+
effects: EffectType[],
|
|
302
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
303
|
+
if (!state.userModes.oper) {
|
|
304
|
+
effects.push(notice(state, 'You must be an oper to use APPROVE.'));
|
|
305
|
+
return { state, effects };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const account = args[0];
|
|
309
|
+
if (account === undefined) {
|
|
310
|
+
effects.push(notice(state, 'Syntax: APPROVE <account>'));
|
|
311
|
+
return { state, effects };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const services = ctx.services as ServicesStore;
|
|
315
|
+
const vhost = services.approveVhost(account);
|
|
316
|
+
if (vhost === undefined) {
|
|
317
|
+
effects.push(notice(state, `No pending vhost request for ${account}.`));
|
|
318
|
+
return { state, effects };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
effects.push(notice(state, `Vhost ${vhost} for ${account} has been approved and activated.`));
|
|
322
|
+
effects.push(
|
|
323
|
+
Effect.sendToNick(account, state.id, [
|
|
324
|
+
servicesRawNotice(
|
|
325
|
+
HOSTSERV_NICK,
|
|
326
|
+
account,
|
|
327
|
+
`Your vhost ${vhost} has been approved and activated. /msg HostServ ON to apply it.`,
|
|
328
|
+
),
|
|
329
|
+
]),
|
|
330
|
+
);
|
|
331
|
+
return { state, effects };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// REJECT (oper-only)
|
|
336
|
+
// ============================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Handles `REJECT <account>`. Drops the pending vhost request without
|
|
340
|
+
* activating it, replies to the oper, and best-effort NOTICEs the
|
|
341
|
+
* requester via `SendToNick` (delivered when they are online, silently
|
|
342
|
+
* dropped otherwise).
|
|
343
|
+
*/
|
|
344
|
+
function handleReject(
|
|
345
|
+
state: ConnectionState,
|
|
346
|
+
args: string[],
|
|
347
|
+
ctx: Ctx,
|
|
348
|
+
effects: EffectType[],
|
|
349
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
350
|
+
if (!state.userModes.oper) {
|
|
351
|
+
effects.push(notice(state, 'You must be an oper to use REJECT.'));
|
|
352
|
+
return { state, effects };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const account = args[0];
|
|
356
|
+
if (account === undefined) {
|
|
357
|
+
effects.push(notice(state, 'Syntax: REJECT <account>'));
|
|
358
|
+
return { state, effects };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const services = ctx.services as ServicesStore;
|
|
362
|
+
const vhost = services.rejectVhost(account);
|
|
363
|
+
if (vhost === undefined) {
|
|
364
|
+
effects.push(notice(state, `No pending vhost request for ${account}.`));
|
|
365
|
+
return { state, effects };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
effects.push(notice(state, `Vhost request for ${account} has been rejected.`));
|
|
369
|
+
effects.push(
|
|
370
|
+
Effect.sendToNick(account, state.id, [
|
|
371
|
+
servicesRawNotice(
|
|
372
|
+
HOSTSERV_NICK,
|
|
373
|
+
account,
|
|
374
|
+
`Your vhost request for ${vhost} has been rejected.`,
|
|
375
|
+
),
|
|
376
|
+
]),
|
|
377
|
+
);
|
|
378
|
+
return { state, effects };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ============================================================================
|
|
382
|
+
// LIST (oper-only)
|
|
383
|
+
// ============================================================================
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Handles `LIST`. Enumerates every pending vhost request as a
|
|
387
|
+
* `<account>: <vhost>` NOTICE line, or a `No pending vhost requests.`
|
|
388
|
+
* NOTICE when the queue is empty.
|
|
389
|
+
*/
|
|
390
|
+
function handleList(
|
|
391
|
+
state: ConnectionState,
|
|
392
|
+
ctx: Ctx,
|
|
393
|
+
effects: EffectType[],
|
|
394
|
+
): { state: ConnectionState; effects: EffectType[] } {
|
|
395
|
+
if (!state.userModes.oper) {
|
|
396
|
+
effects.push(notice(state, 'You must be an oper to use LIST.'));
|
|
397
|
+
return { state, effects };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const services = ctx.services as ServicesStore;
|
|
401
|
+
const pending = services.listPendingVhosts();
|
|
402
|
+
if (pending.length === 0) {
|
|
403
|
+
effects.push(notice(state, 'No pending vhost requests.'));
|
|
404
|
+
return { state, effects };
|
|
405
|
+
}
|
|
406
|
+
for (const entry of pending) {
|
|
407
|
+
effects.push(notice(state, `${entry.account}: ${entry.vhost}`));
|
|
408
|
+
}
|
|
409
|
+
return { state, effects };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// vhost application helpers
|
|
414
|
+
// ============================================================================
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Consults the bound {@link ServicesStore} for an assigned vhost for `account`
|
|
418
|
+
* and, when one is present, applies it to {@link ConnectionState} via
|
|
419
|
+
* {@link applyVhost}. This is the shared login-time hook invoked by every
|
|
420
|
+
* account-success path (SASL PLAIN, PASS-auth via {@link applyAccountSuccess},
|
|
421
|
+
* and NickServ `IDENTIFY` via {@link applyIdentify}) so a reconnecting user
|
|
422
|
+
* with an assigned vhost does not have to re-run `HostServ ON` every session.
|
|
423
|
+
*
|
|
424
|
+
* No-op (no effects emitted, no state change) when no vhost is recorded. The
|
|
425
|
+
* CHGHOST fanout appended to `effects` is empty at SASL / PASS-auth time
|
|
426
|
+
* (registration has not completed and/or no channels are joined yet) but fires
|
|
427
|
+
* for a post-registration NickServ `IDENTIFY` when the user is already in
|
|
428
|
+
* channels — reusing {@link applyVhost} so the wire shape matches `HostServ ON`.
|
|
429
|
+
*/
|
|
430
|
+
export function applyAssignedVhost(
|
|
431
|
+
state: ConnectionState,
|
|
432
|
+
services: ServicesStore,
|
|
433
|
+
account: string,
|
|
434
|
+
effects: EffectType[],
|
|
435
|
+
): void {
|
|
436
|
+
const vhost = services.getVhost(account);
|
|
437
|
+
if (vhost === undefined) return;
|
|
438
|
+
applyVhost(state, vhost, effects);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Applies `vhost` to {@link ConnectionState.host}, records the prior host
|
|
443
|
+
* on {@link ConnectionState.preVhostHost}, flips
|
|
444
|
+
* {@link ConnectionState.vhostActive}, and appends the CHGHOST fanout for
|
|
445
|
+
* every channel the user is joined to. The CHGHOST emission reuses the
|
|
446
|
+
* {@link emitChghost} helper so the wire shape matches every other
|
|
447
|
+
* user/host mutation.
|
|
448
|
+
*/
|
|
449
|
+
export function applyVhost(state: ConnectionState, vhost: string, effects: EffectType[]): void {
|
|
450
|
+
const oldUser = state.user ?? '';
|
|
451
|
+
const oldHost = state.host ?? '';
|
|
452
|
+
state.preVhostHost = oldHost;
|
|
453
|
+
state.host = vhost;
|
|
454
|
+
state.vhostActive = true;
|
|
455
|
+
effects.push(...emitChghost({ conn: state, oldUser, oldHost, newUser: oldUser, newHost: vhost }));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Restores {@link ConnectionState.host} from
|
|
460
|
+
* {@link ConnectionState.preVhostHost}, clears the saved host, flips
|
|
461
|
+
* {@link ConnectionState.vhostActive} off, and appends the CHGHOST fanout.
|
|
462
|
+
*/
|
|
463
|
+
function restoreRealHost(state: ConnectionState, effects: EffectType[]): void {
|
|
464
|
+
const oldUser = state.user ?? '';
|
|
465
|
+
const oldHost = state.host ?? '';
|
|
466
|
+
const realHost = state.preVhostHost ?? oldHost;
|
|
467
|
+
state.host = realHost;
|
|
468
|
+
state.vhostActive = false;
|
|
469
|
+
// biome-ignore lint/performance/noDelete: exactOptionalPropertyTypes forbids `= undefined`.
|
|
470
|
+
delete state.preVhostHost;
|
|
471
|
+
effects.push(
|
|
472
|
+
...emitChghost({ conn: state, oldUser, oldHost, newUser: oldUser, newHost: realHost }),
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ============================================================================
|
|
477
|
+
// Notice helpers
|
|
478
|
+
// ============================================================================
|
|
479
|
+
|
|
480
|
+
/** Builds a `:HostServ!HostServ@services NOTICE <nick> :<text>` line. */
|
|
481
|
+
function notice(state: ConnectionState, text: string): EffectType {
|
|
482
|
+
return Effect.send(state.id, [servicesNotice(HOSTSERV_NICK, state, text)]);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** User-visible HostServ subcommands (shown to every caller). */
|
|
486
|
+
const USER_COMMANDS = 'ON, OFF, REQUEST';
|
|
487
|
+
|
|
488
|
+
/** Oper-only HostServ subcommands, appended to {@link USER_COMMANDS} for opers. */
|
|
489
|
+
const OPER_COMMANDS = 'SET, APPROVE, REJECT, LIST';
|
|
490
|
+
|
|
491
|
+
/** Builds a help `:HostServ!HostServ@services NOTICE <nick> :…` line. */
|
|
492
|
+
function helpNotice(state: ConnectionState): EffectType {
|
|
493
|
+
const commands =
|
|
494
|
+
state.userModes.oper === true ? `${USER_COMMANDS}, ${OPER_COMMANDS}` : USER_COMMANDS;
|
|
495
|
+
return notice(state, `Available commands: ${commands}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function unknownNotice(state: ConnectionState): EffectType {
|
|
499
|
+
const commands =
|
|
500
|
+
state.userModes.oper === true ? `${USER_COMMANDS}, ${OPER_COMMANDS}` : USER_COMMANDS;
|
|
501
|
+
return notice(state, `Unknown command. Available: ${commands}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Formats a services NOTICE line. The source hostmask is
|
|
506
|
+
* `<svc>!<svc>@services` regardless of the deployment's serverName so the
|
|
507
|
+
* pseudo-clients are stable across deployments and easy to identify in
|
|
508
|
+
* client UIs.
|
|
509
|
+
*/
|
|
510
|
+
function servicesNotice(svc: string, state: ConnectionState, text: string): RawLine {
|
|
511
|
+
const nick = state.nick ?? '*';
|
|
512
|
+
return { text: `:${svc}!${svc}@${SERVICES_HOST} NOTICE ${nick} :${text}` };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Formats a services NOTICE line addressed to `recipient` (rather than to
|
|
517
|
+
* the calling connection's nick). Used by APPROVE / REJECT to best-effort
|
|
518
|
+
* NOTICE the requester via `SendToNick` when they are online.
|
|
519
|
+
*/
|
|
520
|
+
function servicesRawNotice(svc: string, recipient: string, text: string): RawLine {
|
|
521
|
+
return { text: `:${svc}!${svc}@${SERVICES_HOST} NOTICE ${recipient} :${text}` };
|
|
522
|
+
}
|
|
@@ -58,6 +58,7 @@ export {
|
|
|
58
58
|
CHATHISTORY_CAP,
|
|
59
59
|
} from './chathistory.js';
|
|
60
60
|
export { tagmsgChannelReducer, tagmsgUserReducer } from './tagmsg.js';
|
|
61
|
+
export { markreadChannelReducer } from './markread.js';
|
|
61
62
|
export {
|
|
62
63
|
DEFAULT_MULTILINE_MAX_BYTES,
|
|
63
64
|
MULTILINE_CAP,
|
|
@@ -98,3 +99,15 @@ export {
|
|
|
98
99
|
persistAway,
|
|
99
100
|
replayPersistedAway,
|
|
100
101
|
} from './pre-away.js';
|
|
102
|
+
export { NICKSERV_NICK, nickservReducer } from './nickserv.js';
|
|
103
|
+
export { MEMOSERV_NICK, memoservReducer } from './memoserv.js';
|
|
104
|
+
export {
|
|
105
|
+
CHANSERV_NICK,
|
|
106
|
+
chanservReducer,
|
|
107
|
+
clearServicesModesDelta,
|
|
108
|
+
positiveMlockLetters,
|
|
109
|
+
setRegisteredDelta,
|
|
110
|
+
} from './chanserv.js';
|
|
111
|
+
export { HOSTSERV_NICK, hostservReducer } from './hostserv.js';
|
|
112
|
+
export { OPERSERV_NICK, operservReducer } from './operserv.js';
|
|
113
|
+
export { SERVICE_NICKS, SHORTCUT_TO_SERVICE } from './service-aliases.js';
|