serverless-ircd 0.7.0 → 0.8.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 +3 -3
- package/.gitmodules +1 -1
- package/CHANGELOG.md +273 -29
- package/README.md +155 -55
- 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-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 +1 -0
- package/apps/local-cli/tests/config-loader.test.ts +14 -0
- package/apps/web/landing/index.html +14 -16
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +23 -16
- package/apps/web/src/build-env.ts +1 -1
- package/apps/web/src/config-schema.ts +6 -6
- package/apps/web/tests/build-smoke.test.ts +14 -14
- package/apps/web/tests/config-schema.test.ts +1 -1
- 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 +546 -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 +5 -0
- 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 +15 -0
- 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/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 +127 -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/handlers.test.ts +105 -3
- 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 +112 -2
- 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.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 +172 -0
- package/packages/irc-core/src/commands/chanserv.ts +882 -0
- package/packages/irc-core/src/commands/hostserv.ts +487 -0
- package/packages/irc-core/src/commands/index.ts +12 -0
- package/packages/irc-core/src/commands/join.ts +164 -8
- 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 +390 -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 +61 -6
- package/packages/irc-core/src/commands/sasl.ts +18 -49
- package/packages/irc-core/src/commands/tagmsg.ts +41 -6
- package/packages/irc-core/src/commands/topic.ts +37 -0
- 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 +21 -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 +1668 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +935 -0
- package/packages/irc-core/tests/commands/join.test.ts +393 -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 +807 -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 +788 -14
- package/packages/irc-core/tests/commands/sasl.test.ts +185 -12
- package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
- 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 +71 -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 +623 -12
- 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 +29 -3
- package/packages/irc-test-support/src/index.ts +1 -0
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +32 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +2 -2
- 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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared account-login helpers used by every account-setting entry point.
|
|
3
|
+
*
|
|
4
|
+
* PLAN §6.5 (services ↔ daemon hooks): a successful SASL PLAIN/EXTERNAL
|
|
5
|
+
* login, a PASS-based account login at registration, and (later) a
|
|
6
|
+
* NickServ `IDENTIFY` all reduce to the same thing — "this connection is
|
|
7
|
+
* now account X". {@link applyAccountSuccess} is that shared reduction:
|
|
8
|
+
* it stamps the account, sets the read-only `+r` user mode, restores
|
|
9
|
+
* per-account state (read markers, persisted away, queued memos), and
|
|
10
|
+
* emits the `900 RPL_LOGGEDIN` + `903 RPL_SASLSUCCESS` numerics plus the
|
|
11
|
+
* `account-notify` broadcast. Keeping the chain in one place guarantees
|
|
12
|
+
* the entry points never diverge.
|
|
13
|
+
*
|
|
14
|
+
* {@link passBasedAccountAuth} is the legacy IRC convention of carrying
|
|
15
|
+
* NickServ credentials in `PASS <nick>:<password>` at registration; it
|
|
16
|
+
* resolves at registration completion via the injected
|
|
17
|
+
* {@link AccountStore} and, on success, delegates to
|
|
18
|
+
* {@link applyAccountSuccess}.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Effect } from '../effects.js';
|
|
22
|
+
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
23
|
+
import { Numerics } from '../protocol/numerics.js';
|
|
24
|
+
import { hostmaskOf } from '../state/connection.js';
|
|
25
|
+
import type { ConnectionState } from '../state/connection.js';
|
|
26
|
+
import type { Ctx } from '../types.js';
|
|
27
|
+
import { emitAccountNotify } from './account-notify.js';
|
|
28
|
+
import { deliverUnreadMemos } from './memoserv.js';
|
|
29
|
+
import { nowAwayLine, replayPersistedAway } from './pre-away.js';
|
|
30
|
+
import { seedReadMarkers } from './read-marker.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Applies the account-login success effects shared by SASL, PASS-auth, and
|
|
34
|
+
* NickServ IDENTIFY.
|
|
35
|
+
*
|
|
36
|
+
* Mutates `state`: sets `state.account`, stamps read-only user mode `r`
|
|
37
|
+
* (`userModes.registered = true`), seeds `lastReadMarkers` from the bound
|
|
38
|
+
* services store, replays the persisted away reason, and returns the
|
|
39
|
+
* effects the caller must emit:
|
|
40
|
+
* - `Send` carrying `900 RPL_LOGGEDIN` + `903 RPL_SASLSUCCESS`;
|
|
41
|
+
* - an optional `306 RPL_NOWAWAY` when an away reason was replayed;
|
|
42
|
+
* - any queued MemoServ `NOTICE`s delivered from the services store;
|
|
43
|
+
* - the `account-notify` `ACCOUNT` broadcasts to cap-enabled peers in
|
|
44
|
+
* shared channels (empty at identify time — no channels joined yet).
|
|
45
|
+
*
|
|
46
|
+
* Does NOT clear SASL scratch state; the SASL reducer owns that.
|
|
47
|
+
*/
|
|
48
|
+
export function applyAccountSuccess(
|
|
49
|
+
state: ConnectionState,
|
|
50
|
+
ctx: Ctx,
|
|
51
|
+
account: string,
|
|
52
|
+
): EffectType[] {
|
|
53
|
+
state.account = account;
|
|
54
|
+
// A successful account login is equivalent to NickServ IDENTIFY: stamp
|
|
55
|
+
// read-only user mode `r` so `221`/WHOIS reflect the identified state.
|
|
56
|
+
state.userModes.registered = true;
|
|
57
|
+
// IRCv3 draft/read-marker: restore the user's per-channel last-read
|
|
58
|
+
// markers from the persisted store so a reconnect resumes at their last
|
|
59
|
+
// read position. No-op when no services store is bound.
|
|
60
|
+
if (ctx.services !== undefined) {
|
|
61
|
+
seedReadMarkers(state, ctx.services, account);
|
|
62
|
+
}
|
|
63
|
+
// IRCv3 draft/pre-away: replay the user's persisted away reason onto the
|
|
64
|
+
// fresh connection and emit 306 so the user sees they are still away.
|
|
65
|
+
const replayedAway =
|
|
66
|
+
ctx.away !== undefined ? replayPersistedAway(state, ctx.away, account) : undefined;
|
|
67
|
+
const awayEffects: EffectType[] =
|
|
68
|
+
replayedAway !== undefined
|
|
69
|
+
? [Effect.send(ctx.connId, [nowAwayLine(state, ctx.serverName)])]
|
|
70
|
+
: [];
|
|
71
|
+
// MemoServ: deliver queued unread memos recorded while the account was
|
|
72
|
+
// offline. No-op when no services store is bound or there are none.
|
|
73
|
+
const memoEffects: EffectType[] = [];
|
|
74
|
+
if (ctx.services !== undefined) {
|
|
75
|
+
deliverUnreadMemos(state, ctx.services, account, memoEffects);
|
|
76
|
+
}
|
|
77
|
+
return [
|
|
78
|
+
Effect.send(ctx.connId, [loggedInLine(ctx, account), saslSuccessLine(ctx)]),
|
|
79
|
+
...awayEffects,
|
|
80
|
+
...memoEffects,
|
|
81
|
+
...emitAccountNotify({ conn: state, account }),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parses a `PASS` payload of the form `<nick>:<password>` (the literal
|
|
87
|
+
* shape used by the `SASL_ACCOUNTS` env var and the seed tooling). Returns
|
|
88
|
+
* `null` when `attempt` is absent, has no colon, or the nick (left of the
|
|
89
|
+
* first colon) is empty. Only the first colon splits; the password may
|
|
90
|
+
* itself contain colons.
|
|
91
|
+
*/
|
|
92
|
+
export function parsePassAccountAttempt(
|
|
93
|
+
attempt: string | undefined,
|
|
94
|
+
): { nick: string; password: string } | null {
|
|
95
|
+
if (attempt === undefined) return null;
|
|
96
|
+
const colon = attempt.indexOf(':');
|
|
97
|
+
if (colon <= 0) return null;
|
|
98
|
+
return { nick: attempt.slice(0, colon), password: attempt.slice(colon + 1) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Attempts PASS-based account authentication at registration completion.
|
|
103
|
+
*
|
|
104
|
+
* Recognises the `<nick>:<password>` form in `state.passAttempt`, verifies
|
|
105
|
+
* it against the bound {@link AccountStore} (PLAIN), and on success runs
|
|
106
|
+
* the shared {@link applyAccountSuccess} chain. Returns the success
|
|
107
|
+
* effects, or `[]` for every other outcome:
|
|
108
|
+
* - already identified (`state.account` set, e.g. via SASL) → no-op;
|
|
109
|
+
* - no `<nick>:<password>` form (bare value) → left to the server-password
|
|
110
|
+
* gate;
|
|
111
|
+
* - payload nick does not match `state.nick` (when set) → not a coherent
|
|
112
|
+
* account attempt, treated as failed auth;
|
|
113
|
+
* - no {@link AccountStore} bound → the `<nick>:<password>` form is
|
|
114
|
+
* ignored (collapses to the server-password gate);
|
|
115
|
+
* - verify failure (unknown nick or wrong password) → no account effects.
|
|
116
|
+
*
|
|
117
|
+
* A failed verify is followed by a verify against a fixed dummy entry so
|
|
118
|
+
* the failure path performs the same AccountStore work whether the payload
|
|
119
|
+
* nick was unknown or the password was wrong — the observable outcome
|
|
120
|
+
* (no numerics, no disconnect, no state change) is identical for both
|
|
121
|
+
* cases, giving no information to a remote attacker.
|
|
122
|
+
*/
|
|
123
|
+
export function passBasedAccountAuth(state: ConnectionState, ctx: Ctx): EffectType[] {
|
|
124
|
+
if (state.account !== undefined) return [];
|
|
125
|
+
const parsed = parsePassAccountAttempt(state.passAttempt);
|
|
126
|
+
if (parsed === null) return [];
|
|
127
|
+
if (state.nick !== undefined && state.nick !== parsed.nick) return [];
|
|
128
|
+
const store = ctx.accounts;
|
|
129
|
+
if (store === undefined) return [];
|
|
130
|
+
const result = store.verify('PLAIN', {
|
|
131
|
+
kind: 'PLAIN',
|
|
132
|
+
username: parsed.nick,
|
|
133
|
+
password: parsed.password,
|
|
134
|
+
});
|
|
135
|
+
if (result.ok) {
|
|
136
|
+
return applyAccountSuccess(state, ctx, result.account);
|
|
137
|
+
}
|
|
138
|
+
// Equalise timing: perform a verify against a fixed dummy entry so a
|
|
139
|
+
// failed PASS-auth always costs one extra AccountStore call regardless
|
|
140
|
+
// of whether the payload nick was unknown or the password was wrong.
|
|
141
|
+
store.verify('PLAIN', {
|
|
142
|
+
kind: 'PLAIN',
|
|
143
|
+
username: PASS_AUTH_DUMMY_NICK,
|
|
144
|
+
password: PASS_AUTH_DUMMY_PASSWORD,
|
|
145
|
+
});
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Fixed dummy credential used to equalise failed-PASS-auth timing. */
|
|
150
|
+
const PASS_AUTH_DUMMY_NICK = '\u0000pass-auth-timing-dummy';
|
|
151
|
+
const PASS_AUTH_DUMMY_PASSWORD = '\u0000pass-auth-timing-dummy';
|
|
152
|
+
|
|
153
|
+
/** Builds the `900 RPL_LOGGEDIN` line for an account login. */
|
|
154
|
+
function loggedInLine(ctx: Ctx, account: string): RawLine {
|
|
155
|
+
const nick = ctx.connection.nick ?? '*';
|
|
156
|
+
const hostmask = hostmaskOf(ctx.connection) ?? nick;
|
|
157
|
+
return {
|
|
158
|
+
text: `:${ctx.serverName} ${numeric(Numerics.RPL_LOGGEDIN)} ${nick} ${hostmask} ${account} :You are now logged in as ${account}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Builds the `903 RPL_SASLSUCCESS` line for an account login. */
|
|
163
|
+
function saslSuccessLine(ctx: Ctx): RawLine {
|
|
164
|
+
return {
|
|
165
|
+
text: `:${ctx.serverName} ${numeric(Numerics.RPL_SASLSUCCESS)} ${ctx.connection.nick ?? '*'} :SASL authentication successful`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Zero-pads a numeric code to its three-digit wire form. */
|
|
170
|
+
function numeric(n: number): string {
|
|
171
|
+
return n.toString().padStart(3, '0');
|
|
172
|
+
}
|