serverless-ircd 0.8.0 → 0.10.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 +4 -0
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +11 -9
- package/.github/workflows/deploy-cf.yml +14 -14
- package/CHANGELOG.md +550 -0
- package/README.md +275 -222
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +82 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +177 -52
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +267 -92
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/package.json +2 -3
- package/apps/cf-tcp-container/src/container-server.ts +33 -10
- 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-tcp-container/wrangler.toml +1 -10
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/wrangler.toml +12 -71
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +115 -48
- 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/favicon.ico +0 -0
- package/apps/web/landing/index.html +227 -3
- package/apps/web/package.json +3 -2
- package/apps/web/scripts/build.mjs +91 -6
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/src/render-docs.ts +292 -0
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +33 -4
- package/apps/web/tests/config-schema.test.ts +149 -25
- 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 +670 -96
- package/docs/AWS-TCP-Deployment.md +20 -45
- package/docs/Cloudflare-Deployment-Guide.md +87 -113
- package/docs/Cloudflare-TCP-Deployment.md +25 -49
- package/docs/Release-Process.md +27 -23
- package/docs/Services.md +102 -23
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +15 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
- package/packages/aws-adapter/src/config-loader.ts +19 -2
- package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
- package/packages/aws-adapter/src/handlers/connect.ts +26 -0
- package/packages/aws-adapter/src/handlers/default.ts +190 -123
- package/packages/aws-adapter/src/handlers/index.ts +67 -23
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +13 -8
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +15 -0
- package/packages/aws-adapter/src/tables.ts +2 -12
- package/packages/aws-adapter/tests/aws-harness.ts +0 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
- package/packages/aws-adapter/tests/connect.test.ts +124 -1
- package/packages/aws-adapter/tests/connection-counter.test.ts +17 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -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 +117 -11
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +20 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +59 -8
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/connection-do.ts +284 -115
- package/packages/cf-adapter/src/d1-services-store.ts +63 -26
- package/packages/cf-adapter/src/env.ts +11 -10
- package/packages/cf-adapter/src/index.ts +0 -6
- package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +222 -51
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/commands/account-auth.ts +60 -35
- package/packages/irc-core/src/commands/chanserv.ts +288 -4
- package/packages/irc-core/src/commands/hostserv.ts +38 -3
- package/packages/irc-core/src/commands/index.ts +1 -0
- package/packages/irc-core/src/commands/join.ts +41 -35
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +138 -15
- package/packages/irc-core/src/commands/registration.ts +28 -17
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/topic.ts +23 -10
- package/packages/irc-core/src/config.ts +35 -9
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +159 -179
- package/packages/irc-core/src/state/channel.ts +17 -0
- package/packages/irc-core/src/types.ts +38 -10
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/commands/chanserv.test.ts +668 -1
- package/packages/irc-core/tests/commands/hostserv.test.ts +71 -0
- package/packages/irc-core/tests/commands/join.test.ts +179 -0
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +422 -3
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +336 -108
- package/packages/irc-core/tests/commands/sasl.test.ts +194 -169
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
- package/packages/irc-core/tests/ports.test.ts +71 -0
- package/packages/irc-core/tests/services-store.test.ts +204 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +80 -44
- package/packages/irc-server/tests/actor.test.ts +384 -50
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +8 -5
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +19 -0
- package/packages/irc-test-support/vitest.config.ts +6 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/src/index.ts +2 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- 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/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- 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 +51 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getLsString } from '../../src/caps/capabilities';
|
|
3
3
|
import { capReducer } from '../../src/commands/cap';
|
|
4
4
|
import { authenticateReducer } from '../../src/commands/sasl';
|
|
5
5
|
import { Effect } from '../../src/effects';
|
|
6
6
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
7
|
-
import type {
|
|
8
|
-
AccountStore,
|
|
9
|
-
AwayStore,
|
|
10
|
-
MtlsIdentityProvider,
|
|
11
|
-
SaslPayload,
|
|
12
|
-
SaslResult,
|
|
13
|
-
ServicesStore,
|
|
14
|
-
} from '../../src/ports';
|
|
7
|
+
import type { AwayStore, MtlsIdentityProvider, ServicesStore } from '../../src/ports';
|
|
15
8
|
import {
|
|
16
9
|
EmptyMotdProvider,
|
|
17
10
|
FakeClock,
|
|
@@ -53,11 +46,24 @@ function saslReadyState(): ConnectionState {
|
|
|
53
46
|
return s;
|
|
54
47
|
}
|
|
55
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Builds a {@link InMemoryServicesStore} with `nick` registered under
|
|
51
|
+
* `password`. The single credential home for SASL PLAIN / EXTERNAL / PASS.
|
|
52
|
+
*/
|
|
53
|
+
function registeredServices(
|
|
54
|
+
nick = 'alice',
|
|
55
|
+
password = 'secret',
|
|
56
|
+
email = 'alice@example.com',
|
|
57
|
+
): InMemoryServicesStore {
|
|
58
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
59
|
+
services.registerNick(nick, password, email);
|
|
60
|
+
return services;
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
function makeCtx(
|
|
57
64
|
state: ConnectionState,
|
|
58
|
-
accounts?: AccountStore,
|
|
59
|
-
mtlsIdentity?: MtlsIdentityProvider,
|
|
60
65
|
services?: ServicesStore,
|
|
66
|
+
mtlsIdentity?: MtlsIdentityProvider,
|
|
61
67
|
away?: AwayStore,
|
|
62
68
|
): Ctx {
|
|
63
69
|
return buildCtx({
|
|
@@ -66,23 +72,12 @@ function makeCtx(
|
|
|
66
72
|
ids: new SequentialIdFactory(),
|
|
67
73
|
motd: EmptyMotdProvider,
|
|
68
74
|
connection: state,
|
|
69
|
-
...(accounts !== undefined ? { accounts } : {}),
|
|
70
|
-
...(mtlsIdentity !== undefined ? { mtlsIdentity } : {}),
|
|
71
75
|
...(services !== undefined ? { services } : {}),
|
|
76
|
+
...(mtlsIdentity !== undefined ? { mtlsIdentity } : {}),
|
|
72
77
|
...(away !== undefined ? { away } : {}),
|
|
73
78
|
});
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
/** A capture-and-respond fake AccountStore. */
|
|
77
|
-
class FakeAccountStore implements AccountStore {
|
|
78
|
-
readonly calls: Array<{ mech: string; payload: SaslPayload }> = [];
|
|
79
|
-
constructor(private readonly result: SaslResult) {}
|
|
80
|
-
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
81
|
-
this.calls.push({ mech, payload });
|
|
82
|
-
return this.result;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
81
|
/** A fake MtlsIdentityProvider that returns a canned identity for one connId. */
|
|
87
82
|
class FakeMtlsIdentityProvider implements MtlsIdentityProvider {
|
|
88
83
|
readonly calls: string[] = [];
|
|
@@ -135,10 +130,10 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
135
130
|
expect(state.saslMech).toBe('PLAIN');
|
|
136
131
|
});
|
|
137
132
|
|
|
138
|
-
it('completes with 900 and 903 when the
|
|
133
|
+
it('completes with 900 and 903 when the ServicesStore accepts the credentials', () => {
|
|
139
134
|
const state = saslReadyState();
|
|
140
|
-
const
|
|
141
|
-
const ctx = makeCtx(state,
|
|
135
|
+
const services = registeredServices('alice', 'secret');
|
|
136
|
+
const ctx = makeCtx(state, services);
|
|
142
137
|
|
|
143
138
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
144
139
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -152,8 +147,8 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
152
147
|
|
|
153
148
|
it('records the authenticated account name on the connection', () => {
|
|
154
149
|
const state = saslReadyState();
|
|
155
|
-
const
|
|
156
|
-
const ctx = makeCtx(state,
|
|
150
|
+
const services = registeredServices('alice', 'secret');
|
|
151
|
+
const ctx = makeCtx(state, services);
|
|
157
152
|
|
|
158
153
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
159
154
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -163,11 +158,10 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
163
158
|
|
|
164
159
|
it('seeds lastReadMarkers from the ServicesStore at identify', () => {
|
|
165
160
|
const state = saslReadyState();
|
|
166
|
-
const
|
|
167
|
-
const services = new InMemoryServicesStore();
|
|
161
|
+
const services = registeredServices('alice', 'secret');
|
|
168
162
|
services.setLastReadMarker('alice', '#foo', 'm1');
|
|
169
163
|
services.setLastReadMarker('alice', '#bar', 'm2');
|
|
170
|
-
const ctx = makeCtx(state,
|
|
164
|
+
const ctx = makeCtx(state, services);
|
|
171
165
|
|
|
172
166
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
173
167
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -178,10 +172,9 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
178
172
|
|
|
179
173
|
it('does not seed markers for a different account', () => {
|
|
180
174
|
const state = saslReadyState();
|
|
181
|
-
const
|
|
182
|
-
const services = new InMemoryServicesStore();
|
|
175
|
+
const services = registeredServices('alice', 'secret');
|
|
183
176
|
services.setLastReadMarker('bob', '#foo', 'm9');
|
|
184
|
-
const ctx = makeCtx(state,
|
|
177
|
+
const ctx = makeCtx(state, services);
|
|
185
178
|
|
|
186
179
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
187
180
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -191,10 +184,10 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
191
184
|
|
|
192
185
|
it('replays the persisted away reason on identify (draft/pre-away)', () => {
|
|
193
186
|
const state = saslReadyState();
|
|
194
|
-
const
|
|
187
|
+
const services = registeredServices('alice', 'secret');
|
|
195
188
|
const away = new InMemoryAwayStore();
|
|
196
189
|
away.set('alice', 'brb');
|
|
197
|
-
const ctx = makeCtx(state,
|
|
190
|
+
const ctx = makeCtx(state, services, undefined, away);
|
|
198
191
|
|
|
199
192
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
200
193
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -204,10 +197,10 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
204
197
|
|
|
205
198
|
it('emits 306 RPL_NOWAWAY after identifying when an away reason is replayed', () => {
|
|
206
199
|
const state = saslReadyState();
|
|
207
|
-
const
|
|
200
|
+
const services = registeredServices('alice', 'secret');
|
|
208
201
|
const away = new InMemoryAwayStore();
|
|
209
202
|
away.set('alice', 'brb');
|
|
210
|
-
const ctx = makeCtx(state,
|
|
203
|
+
const ctx = makeCtx(state, services, undefined, away);
|
|
211
204
|
|
|
212
205
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
213
206
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -220,9 +213,9 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
220
213
|
|
|
221
214
|
it('does not replay an away reason when none is persisted', () => {
|
|
222
215
|
const state = saslReadyState();
|
|
223
|
-
const
|
|
216
|
+
const services = registeredServices('alice', 'secret');
|
|
224
217
|
const away = new InMemoryAwayStore();
|
|
225
|
-
const ctx = makeCtx(state,
|
|
218
|
+
const ctx = makeCtx(state, services, undefined, away);
|
|
226
219
|
|
|
227
220
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
228
221
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -234,8 +227,8 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
234
227
|
|
|
235
228
|
it('does not replay an away reason when no AwayStore is bound', () => {
|
|
236
229
|
const state = saslReadyState();
|
|
237
|
-
const
|
|
238
|
-
const ctx = makeCtx(state,
|
|
230
|
+
const services = registeredServices('alice', 'secret');
|
|
231
|
+
const ctx = makeCtx(state, services); // no away store
|
|
239
232
|
|
|
240
233
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
241
234
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -247,10 +240,10 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
247
240
|
|
|
248
241
|
it('looks up the persisted away reason case-insensitively', () => {
|
|
249
242
|
const state = saslReadyState();
|
|
250
|
-
const
|
|
243
|
+
const services = registeredServices('alice', 'secret');
|
|
251
244
|
const away = new InMemoryAwayStore();
|
|
252
245
|
away.set('ALICE', 'brb');
|
|
253
|
-
const ctx = makeCtx(state,
|
|
246
|
+
const ctx = makeCtx(state, services, undefined, away);
|
|
254
247
|
|
|
255
248
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
256
249
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -260,10 +253,9 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
260
253
|
|
|
261
254
|
it('delivers queued unread memos on a successful SASL login', () => {
|
|
262
255
|
const state = saslReadyState();
|
|
263
|
-
const
|
|
264
|
-
const services = new InMemoryServicesStore();
|
|
256
|
+
const services = registeredServices('alice', 'secret');
|
|
265
257
|
services.recordMemo({ from: 'bob', to: 'alice', body: 'Hi alice!', sentAt: 1_000 });
|
|
266
|
-
const ctx = makeCtx(state,
|
|
258
|
+
const ctx = makeCtx(state, services);
|
|
267
259
|
|
|
268
260
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
269
261
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -276,10 +268,9 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
276
268
|
|
|
277
269
|
it('marks delivered memos as read after a successful SASL login', () => {
|
|
278
270
|
const state = saslReadyState();
|
|
279
|
-
const
|
|
280
|
-
const services = new InMemoryServicesStore();
|
|
271
|
+
const services = registeredServices('alice', 'secret');
|
|
281
272
|
services.recordMemo({ from: 'bob', to: 'alice', body: 'Hi alice!', sentAt: 1_000 });
|
|
282
|
-
const ctx = makeCtx(state,
|
|
273
|
+
const ctx = makeCtx(state, services);
|
|
283
274
|
|
|
284
275
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
285
276
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -289,11 +280,10 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
289
280
|
|
|
290
281
|
it('does not deliver already-read memos on a SASL login', () => {
|
|
291
282
|
const state = saslReadyState();
|
|
292
|
-
const
|
|
293
|
-
const services = new InMemoryServicesStore();
|
|
283
|
+
const services = registeredServices('alice', 'secret');
|
|
294
284
|
services.recordMemo({ from: 'bob', to: 'alice', body: 'old', sentAt: 1_000 });
|
|
295
285
|
services.markMemoRead('alice', 1);
|
|
296
|
-
const ctx = makeCtx(state,
|
|
286
|
+
const ctx = makeCtx(state, services);
|
|
297
287
|
|
|
298
288
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
299
289
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -302,11 +292,51 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
302
292
|
expect(texts.some((l) => l.text.includes('[Memo #1'))).toBe(false);
|
|
303
293
|
});
|
|
304
294
|
|
|
295
|
+
it('applies the assigned HostServ vhost on SASL PLAIN success', () => {
|
|
296
|
+
const state = saslReadyState();
|
|
297
|
+
state.host = 'real.example.net';
|
|
298
|
+
const services = registeredServices('alice', 'secret');
|
|
299
|
+
services.setVhost('alice', 'cool.example.net');
|
|
300
|
+
const ctx = makeCtx(state, services);
|
|
301
|
+
|
|
302
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
303
|
+
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
304
|
+
|
|
305
|
+
expect(state.vhostActive).toBe(true);
|
|
306
|
+
expect(state.host).toBe('cool.example.net');
|
|
307
|
+
expect(state.preVhostHost).toBe('real.example.net');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('does not emit a CHGHOST broadcast on SASL success (pre-registration, no peers)', () => {
|
|
311
|
+
const state = saslReadyState();
|
|
312
|
+
state.host = 'real.example.net';
|
|
313
|
+
const services = registeredServices('alice', 'secret');
|
|
314
|
+
services.setVhost('alice', 'cool.example.net');
|
|
315
|
+
const ctx = makeCtx(state, services);
|
|
316
|
+
|
|
317
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
318
|
+
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
319
|
+
|
|
320
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toEqual<EffectType[]>([]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('leaves host unchanged on SASL success when no vhost is assigned', () => {
|
|
324
|
+
const state = saslReadyState();
|
|
325
|
+
state.host = 'real.example.net';
|
|
326
|
+
const services = registeredServices('alice', 'secret');
|
|
327
|
+
const ctx = makeCtx(state, services);
|
|
328
|
+
|
|
329
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
330
|
+
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
331
|
+
|
|
332
|
+
expect(state.vhostActive).toBeUndefined();
|
|
333
|
+
expect(state.host).toBe('real.example.net');
|
|
334
|
+
});
|
|
335
|
+
|
|
305
336
|
it('delivers nothing on a SASL login when the account has no memos', () => {
|
|
306
337
|
const state = saslReadyState();
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const ctx = makeCtx(state, accounts, undefined, services);
|
|
338
|
+
const services = registeredServices('alice', 'secret');
|
|
339
|
+
const ctx = makeCtx(state, services);
|
|
310
340
|
|
|
311
341
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
312
342
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -317,10 +347,9 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
317
347
|
|
|
318
348
|
it('looks up queued memos case-insensitively on a SASL login', () => {
|
|
319
349
|
const state = saslReadyState();
|
|
320
|
-
const
|
|
321
|
-
const services = new InMemoryServicesStore();
|
|
350
|
+
const services = registeredServices('alice', 'secret');
|
|
322
351
|
services.recordMemo({ from: 'bob', to: 'ALICE', body: 'folded', sentAt: 1_000 });
|
|
323
|
-
const ctx = makeCtx(state,
|
|
352
|
+
const ctx = makeCtx(state, services);
|
|
324
353
|
|
|
325
354
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
326
355
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -333,8 +362,7 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
333
362
|
|
|
334
363
|
it('does not deliver memos when no ServicesStore is bound', () => {
|
|
335
364
|
const state = saslReadyState();
|
|
336
|
-
const
|
|
337
|
-
const ctx = makeCtx(state, accounts);
|
|
365
|
+
const ctx = makeCtx(state);
|
|
338
366
|
|
|
339
367
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
340
368
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -345,8 +373,8 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
345
373
|
|
|
346
374
|
it('clears the in-progress mechanism after a successful authentication', () => {
|
|
347
375
|
const state = saslReadyState();
|
|
348
|
-
const
|
|
349
|
-
const ctx = makeCtx(state,
|
|
376
|
+
const services = registeredServices('alice', 'secret');
|
|
377
|
+
const ctx = makeCtx(state, services);
|
|
350
378
|
|
|
351
379
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
352
380
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -354,40 +382,36 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
354
382
|
expect(state.saslMech).toBeUndefined();
|
|
355
383
|
});
|
|
356
384
|
|
|
357
|
-
it('forwards parsed PLAIN credentials to
|
|
385
|
+
it('forwards parsed PLAIN credentials to services.verifyNick', () => {
|
|
358
386
|
const state = saslReadyState();
|
|
359
|
-
const
|
|
360
|
-
const
|
|
387
|
+
const services = registeredServices('alice', 'secret');
|
|
388
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
389
|
+
const ctx = makeCtx(state, services);
|
|
361
390
|
|
|
362
391
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
363
392
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
364
393
|
|
|
365
|
-
expect(
|
|
366
|
-
{ mech: 'PLAIN', payload: { kind: 'PLAIN', username: 'alice', password: 'secret' } },
|
|
367
|
-
]);
|
|
394
|
+
expect(verifyNick).toHaveBeenCalledWith('alice', 'secret');
|
|
368
395
|
});
|
|
369
396
|
|
|
370
397
|
it('ignores the authorization identity field in the PLAIN payload', () => {
|
|
371
398
|
const state = saslReadyState();
|
|
372
|
-
const
|
|
373
|
-
const
|
|
399
|
+
const services = registeredServices('alice', 'secret');
|
|
400
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
401
|
+
const ctx = makeCtx(state, services);
|
|
374
402
|
|
|
375
403
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
376
404
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret', 'bob')), ctx);
|
|
377
405
|
|
|
378
|
-
expect(
|
|
379
|
-
kind: 'PLAIN',
|
|
380
|
-
username: 'alice',
|
|
381
|
-
password: 'secret',
|
|
382
|
-
});
|
|
406
|
+
expect(verifyNick).toHaveBeenCalledWith('alice', 'secret');
|
|
383
407
|
});
|
|
384
408
|
|
|
385
409
|
it('targets `*` with an empty hostmask when the client has no nick yet', () => {
|
|
386
410
|
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
387
411
|
state.capNegotiating = true;
|
|
388
412
|
state.caps.add('sasl');
|
|
389
|
-
const
|
|
390
|
-
const ctx = makeCtx(state,
|
|
413
|
+
const services = registeredServices('alice', 'secret');
|
|
414
|
+
const ctx = makeCtx(state, services);
|
|
391
415
|
|
|
392
416
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
393
417
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -405,10 +429,10 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
405
429
|
// ============================================================================
|
|
406
430
|
|
|
407
431
|
describe('authenticateReducer — PLAIN failure', () => {
|
|
408
|
-
it('emits 904 ERR_SASLFAIL when the
|
|
432
|
+
it('emits 904 ERR_SASLFAIL when the ServicesStore rejects the credentials', () => {
|
|
409
433
|
const state = saslReadyState();
|
|
410
|
-
const
|
|
411
|
-
const ctx = makeCtx(state,
|
|
434
|
+
const services = registeredServices('alice', 'secret');
|
|
435
|
+
const ctx = makeCtx(state, services);
|
|
412
436
|
|
|
413
437
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
414
438
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'wrong')), ctx);
|
|
@@ -420,8 +444,8 @@ describe('authenticateReducer — PLAIN failure', () => {
|
|
|
420
444
|
|
|
421
445
|
it('does not record an account on failure', () => {
|
|
422
446
|
const state = saslReadyState();
|
|
423
|
-
const
|
|
424
|
-
const ctx = makeCtx(state,
|
|
447
|
+
const services = registeredServices('alice', 'secret');
|
|
448
|
+
const ctx = makeCtx(state, services);
|
|
425
449
|
|
|
426
450
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
427
451
|
authenticateReducer(state, authenticate(plainPayload('alice', 'wrong')), ctx);
|
|
@@ -431,8 +455,8 @@ describe('authenticateReducer — PLAIN failure', () => {
|
|
|
431
455
|
|
|
432
456
|
it('clears the in-progress mechanism after a failure', () => {
|
|
433
457
|
const state = saslReadyState();
|
|
434
|
-
const
|
|
435
|
-
const ctx = makeCtx(state,
|
|
458
|
+
const services = registeredServices('alice', 'secret');
|
|
459
|
+
const ctx = makeCtx(state, services);
|
|
436
460
|
|
|
437
461
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
438
462
|
authenticateReducer(state, authenticate(plainPayload('alice', 'wrong')), ctx);
|
|
@@ -442,11 +466,11 @@ describe('authenticateReducer — PLAIN failure', () => {
|
|
|
442
466
|
});
|
|
443
467
|
|
|
444
468
|
// ============================================================================
|
|
445
|
-
// AUTHENTICATE PLAIN — no
|
|
469
|
+
// AUTHENTICATE PLAIN — no ServicesStore wired
|
|
446
470
|
// ============================================================================
|
|
447
471
|
|
|
448
|
-
describe('authenticateReducer — no
|
|
449
|
-
it('emits 904 when the PLAIN payload arrives but no
|
|
472
|
+
describe('authenticateReducer — no ServicesStore configured', () => {
|
|
473
|
+
it('emits 904 when the PLAIN payload arrives but no ServicesStore is wired', () => {
|
|
450
474
|
const state = saslReadyState();
|
|
451
475
|
const ctx = makeCtx(state);
|
|
452
476
|
|
|
@@ -487,28 +511,41 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
487
511
|
|
|
488
512
|
it('succeeds (900/903) when a valid cert identity is presented', () => {
|
|
489
513
|
const state = saslReadyState();
|
|
490
|
-
const
|
|
514
|
+
const services = registeredServices('alice', 'secret');
|
|
515
|
+
services.addCertFP('alice', 'CN=alice');
|
|
491
516
|
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
492
|
-
const ctx = makeCtx(state,
|
|
517
|
+
const ctx = makeCtx(state, services, provider);
|
|
493
518
|
|
|
494
519
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
495
520
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
496
521
|
|
|
497
522
|
expect(state.account).toBe('alice');
|
|
498
|
-
expect(accounts.calls).toEqual([
|
|
499
|
-
{ mech: 'EXTERNAL', payload: { kind: 'EXTERNAL', identity: 'CN=alice' } },
|
|
500
|
-
]);
|
|
501
523
|
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
502
524
|
L(`:${SV} 900 alice alice!alice@example.com alice :You are now logged in as alice`),
|
|
503
525
|
L(`:${SV} 903 alice :SASL authentication successful`),
|
|
504
526
|
]);
|
|
505
527
|
});
|
|
506
528
|
|
|
529
|
+
it('forwards the resolved mTLS subject to services.verifyCertFP', () => {
|
|
530
|
+
const state = saslReadyState();
|
|
531
|
+
const services = registeredServices('alice', 'secret');
|
|
532
|
+
services.addCertFP('alice', 'CN=alice');
|
|
533
|
+
const verifyCertFP = vi.spyOn(services, 'verifyCertFP');
|
|
534
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
535
|
+
const ctx = makeCtx(state, services, provider);
|
|
536
|
+
|
|
537
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
538
|
+
authenticateReducer(state, authenticate('+'), ctx);
|
|
539
|
+
|
|
540
|
+
expect(verifyCertFP).toHaveBeenCalledWith('CN=alice');
|
|
541
|
+
});
|
|
542
|
+
|
|
507
543
|
it('clears saslMech/saslBuffer after a successful EXTERNAL', () => {
|
|
508
544
|
const state = saslReadyState();
|
|
509
|
-
const
|
|
545
|
+
const services = registeredServices('alice', 'secret');
|
|
546
|
+
services.addCertFP('alice', 'CN=alice');
|
|
510
547
|
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
511
|
-
const ctx = makeCtx(state,
|
|
548
|
+
const ctx = makeCtx(state, services, provider);
|
|
512
549
|
|
|
513
550
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
514
551
|
authenticateReducer(state, authenticate('+'), ctx);
|
|
@@ -519,11 +556,11 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
519
556
|
|
|
520
557
|
it('delivers queued memos on a successful EXTERNAL login', () => {
|
|
521
558
|
const state = saslReadyState();
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
const services = new InMemoryServicesStore();
|
|
559
|
+
const services = registeredServices('alice', 'secret');
|
|
560
|
+
services.addCertFP('alice', 'CN=alice');
|
|
525
561
|
services.recordMemo({ from: 'bob', to: 'alice', body: 'via mTLS', sentAt: 1_000 });
|
|
526
|
-
const
|
|
562
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
563
|
+
const ctx = makeCtx(state, services, provider);
|
|
527
564
|
|
|
528
565
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
529
566
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
@@ -537,9 +574,10 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
537
574
|
|
|
538
575
|
it('fails with 904 when the provider has no identity for the connection', () => {
|
|
539
576
|
const state = saslReadyState();
|
|
540
|
-
const
|
|
577
|
+
const services = registeredServices('alice', 'secret');
|
|
578
|
+
services.addCertFP('alice', 'CN=alice');
|
|
541
579
|
const provider = new FakeMtlsIdentityProvider('c1', undefined);
|
|
542
|
-
const ctx = makeCtx(state,
|
|
580
|
+
const ctx = makeCtx(state, services, provider);
|
|
543
581
|
|
|
544
582
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
545
583
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
@@ -550,11 +588,12 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
550
588
|
expect(state.account).toBeUndefined();
|
|
551
589
|
});
|
|
552
590
|
|
|
553
|
-
it('fails with 904 when the
|
|
591
|
+
it('fails with 904 when the subject is not attached to any account', () => {
|
|
554
592
|
const state = saslReadyState();
|
|
555
|
-
const
|
|
593
|
+
const services = registeredServices('alice', 'secret');
|
|
594
|
+
// No cert subject attached to alice; CN=evil is unknown to the store.
|
|
556
595
|
const provider = new FakeMtlsIdentityProvider('c1', 'CN=evil');
|
|
557
|
-
const ctx = makeCtx(state,
|
|
596
|
+
const ctx = makeCtx(state, services, provider);
|
|
558
597
|
|
|
559
598
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
560
599
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
@@ -562,9 +601,27 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
562
601
|
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
563
602
|
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
564
603
|
]);
|
|
604
|
+
expect(state.account).toBeUndefined();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('does not leak account existence on an unknown subject (no state change)', () => {
|
|
608
|
+
const state = saslReadyState();
|
|
609
|
+
const services = registeredServices('alice', 'secret');
|
|
610
|
+
services.addCertFP('alice', 'CN=alice');
|
|
611
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=evil');
|
|
612
|
+
const ctx = makeCtx(state, services, provider);
|
|
613
|
+
|
|
614
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
615
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
616
|
+
|
|
617
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
618
|
+
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
619
|
+
]);
|
|
620
|
+
expect(state.account).toBeUndefined();
|
|
621
|
+
expect(state.saslMech).toBeUndefined();
|
|
565
622
|
});
|
|
566
623
|
|
|
567
|
-
it('fails with 904 when no
|
|
624
|
+
it('fails with 904 when no ServicesStore is configured', () => {
|
|
568
625
|
const state = saslReadyState();
|
|
569
626
|
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
570
627
|
const ctx = makeCtx(state, undefined, provider);
|
|
@@ -630,8 +687,8 @@ describe('authenticateReducer — abort / already-authenticated', () => {
|
|
|
630
687
|
|
|
631
688
|
it('emits 907 when sent after a successful SASL authentication', () => {
|
|
632
689
|
const state = saslReadyState();
|
|
633
|
-
const
|
|
634
|
-
const ctx = makeCtx(state,
|
|
690
|
+
const services = registeredServices('alice', 'secret');
|
|
691
|
+
const ctx = makeCtx(state, services);
|
|
635
692
|
|
|
636
693
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
637
694
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -692,13 +749,14 @@ describe('authenticateReducer — client abort', () => {
|
|
|
692
749
|
describe('authenticateReducer — empty payload (`+`)', () => {
|
|
693
750
|
it('treats `AUTHENTICATE +` as the final (empty) chunk and rejects malformed PLAIN', () => {
|
|
694
751
|
const state = saslReadyState();
|
|
695
|
-
const
|
|
696
|
-
const
|
|
752
|
+
const services = registeredServices('alice', 'secret');
|
|
753
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
754
|
+
const ctx = makeCtx(state, services);
|
|
697
755
|
|
|
698
756
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
699
757
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
700
758
|
|
|
701
|
-
expect(
|
|
759
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
702
760
|
expect(firstLineText(out.effects[0])).toContain(' 904 ');
|
|
703
761
|
expect(state.saslMech).toBeUndefined();
|
|
704
762
|
});
|
|
@@ -711,8 +769,9 @@ describe('authenticateReducer — empty payload (`+`)', () => {
|
|
|
711
769
|
describe('authenticateReducer — chunked payload', () => {
|
|
712
770
|
it('buffers chunks of exactly 400 bytes and verifies on the short final chunk', () => {
|
|
713
771
|
const state = saslReadyState();
|
|
714
|
-
const
|
|
715
|
-
const
|
|
772
|
+
const services = registeredServices('alice', 'p'.repeat(320));
|
|
773
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
774
|
+
const ctx = makeCtx(state, services);
|
|
716
775
|
|
|
717
776
|
const longPassword = 'p'.repeat(320);
|
|
718
777
|
const payload = plainPayload('alice', longPassword);
|
|
@@ -727,19 +786,17 @@ describe('authenticateReducer — chunked payload', () => {
|
|
|
727
786
|
const buffered = authenticateReducer(state, authenticate(first), ctx);
|
|
728
787
|
expect(buffered.effects).toEqual([]);
|
|
729
788
|
expect(state.saslMech).toBe('PLAIN');
|
|
730
|
-
expect(
|
|
789
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
731
790
|
|
|
732
791
|
authenticateReducer(state, authenticate(rest), ctx);
|
|
733
|
-
expect(
|
|
734
|
-
{ mech: 'PLAIN', payload: { kind: 'PLAIN', username: 'alice', password: longPassword } },
|
|
735
|
-
]);
|
|
792
|
+
expect(verifyNick).toHaveBeenCalledWith('alice', longPassword);
|
|
736
793
|
expect(state.account).toBe('alice');
|
|
737
794
|
});
|
|
738
795
|
|
|
739
796
|
it('rejects an oversized payload with 905 ERR_SASLTOOLONG', () => {
|
|
740
797
|
const state = saslReadyState();
|
|
741
|
-
const
|
|
742
|
-
const ctx = makeCtx(state,
|
|
798
|
+
const services = registeredServices('alice', 'secret');
|
|
799
|
+
const ctx = makeCtx(state, services);
|
|
743
800
|
|
|
744
801
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
745
802
|
const out = authenticateReducer(state, authenticate(`${'A'.repeat(8193)}`), ctx);
|
|
@@ -758,37 +815,40 @@ describe('authenticateReducer — chunked payload', () => {
|
|
|
758
815
|
describe('authenticateReducer — malformed PLAIN', () => {
|
|
759
816
|
it('emits 904 when the base64 payload does not decode to three NUL-separated fields', () => {
|
|
760
817
|
const state = saslReadyState();
|
|
761
|
-
const
|
|
762
|
-
const
|
|
818
|
+
const services = registeredServices('alice', 'secret');
|
|
819
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
820
|
+
const ctx = makeCtx(state, services);
|
|
763
821
|
|
|
764
822
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
765
823
|
const out = authenticateReducer(state, authenticate(encodeBase64('onlyonefield')), ctx);
|
|
766
824
|
|
|
767
825
|
expect(firstLineText(out.effects[0])).toContain(' 904 ');
|
|
768
|
-
expect(
|
|
826
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
769
827
|
});
|
|
770
828
|
|
|
771
829
|
it('emits 904 when the payload is not valid base64', () => {
|
|
772
830
|
const state = saslReadyState();
|
|
773
|
-
const
|
|
774
|
-
const
|
|
831
|
+
const services = registeredServices('alice', 'secret');
|
|
832
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
833
|
+
const ctx = makeCtx(state, services);
|
|
775
834
|
|
|
776
835
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
777
836
|
const out = authenticateReducer(state, authenticate('!!!notbase64!!!'), ctx);
|
|
778
837
|
|
|
779
838
|
expect(firstLineText(out.effects[0])).toContain(' 904 ');
|
|
780
|
-
expect(
|
|
839
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
781
840
|
});
|
|
782
841
|
|
|
783
842
|
it('emits 908 when the first AUTHENTICATE frame is not a recognized mechanism', () => {
|
|
784
843
|
const state = saslReadyState();
|
|
785
|
-
const
|
|
786
|
-
const
|
|
844
|
+
const services = registeredServices('alice', 'secret');
|
|
845
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
846
|
+
const ctx = makeCtx(state, services);
|
|
787
847
|
|
|
788
848
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
789
849
|
|
|
790
850
|
expect(firstLineText(out.effects[0])).toContain(' 908 ');
|
|
791
|
-
expect(
|
|
851
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
792
852
|
});
|
|
793
853
|
});
|
|
794
854
|
|
|
@@ -835,8 +895,8 @@ describe('authenticateReducer — CAP REQ sasl integration', () => {
|
|
|
835
895
|
state.user = 'alice';
|
|
836
896
|
state.host = 'example.com';
|
|
837
897
|
state.registration = 'registering';
|
|
838
|
-
const
|
|
839
|
-
const ctx = makeCtx(state,
|
|
898
|
+
const services = registeredServices('alice', 'secret');
|
|
899
|
+
const ctx = makeCtx(state, services);
|
|
840
900
|
|
|
841
901
|
capReducer(state, { command: 'CAP', params: ['REQ', 'sasl'], tags: {} }, ctx);
|
|
842
902
|
expect(state.caps.has('sasl')).toBe(true);
|
|
@@ -861,15 +921,15 @@ describe('authenticateReducer — CAP REQ sasl integration', () => {
|
|
|
861
921
|
});
|
|
862
922
|
|
|
863
923
|
// ============================================================================
|
|
864
|
-
// SASL PLAIN — NickServ-registered accounts (
|
|
924
|
+
// SASL PLAIN — NickServ-registered accounts (single credential home)
|
|
865
925
|
// ============================================================================
|
|
866
926
|
|
|
867
927
|
describe('authenticateReducer — NickServ-registered nick via ServicesStore', () => {
|
|
868
|
-
it('succeeds with credentials registered via NickServ
|
|
928
|
+
it('succeeds with credentials registered via NickServ (no second registration)', () => {
|
|
869
929
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
870
930
|
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
871
931
|
const state = saslReadyState();
|
|
872
|
-
const ctx = makeCtx(state,
|
|
932
|
+
const ctx = makeCtx(state, services);
|
|
873
933
|
|
|
874
934
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
875
935
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'hunter2')), ctx);
|
|
@@ -881,46 +941,11 @@ describe('authenticateReducer — NickServ-registered nick via ServicesStore', (
|
|
|
881
941
|
);
|
|
882
942
|
});
|
|
883
943
|
|
|
884
|
-
it('
|
|
885
|
-
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
886
|
-
services.registerNick('bob', 'bobpass', 'bob@example.com');
|
|
887
|
-
// Static AccountStore does not know about bob; the services store does.
|
|
888
|
-
const accounts = new FakeAccountStore({ ok: false, reason: 'invalid credentials' });
|
|
889
|
-
const state = saslReadyState();
|
|
890
|
-
state.nick = 'bob';
|
|
891
|
-
state.user = 'bob';
|
|
892
|
-
state.host = 'example.com';
|
|
893
|
-
const ctx = makeCtx(state, accounts, undefined, services);
|
|
894
|
-
|
|
895
|
-
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
896
|
-
const out = authenticateReducer(state, authenticate(plainPayload('bob', 'bobpass')), ctx);
|
|
897
|
-
|
|
898
|
-
expect(state.account).toBe('bob');
|
|
899
|
-
expect(sendLinesOf(out.effects[0]).map((l) => l.text)).toContain(
|
|
900
|
-
`:${SV} 903 bob :SASL authentication successful`,
|
|
901
|
-
);
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
it('prefers the static AccountStore over the services store on a match', () => {
|
|
905
|
-
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
906
|
-
services.registerNick('alice', 'servicespw', 'alice@example.com');
|
|
907
|
-
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
908
|
-
const state = saslReadyState();
|
|
909
|
-
const ctx = makeCtx(state, accounts, undefined, services);
|
|
910
|
-
|
|
911
|
-
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
912
|
-
authenticateReducer(state, authenticate(plainPayload('alice', 'staticpw')), ctx);
|
|
913
|
-
|
|
914
|
-
// The static store vouched for the account; it is used.
|
|
915
|
-
expect(accounts.calls).toHaveLength(1);
|
|
916
|
-
expect(state.account).toBe('alice');
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
it('fails when neither the AccountStore nor the ServicesStore verifies the credential', () => {
|
|
944
|
+
it('fails when neither the nick nor a matching credential verifies', () => {
|
|
920
945
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
921
946
|
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
922
947
|
const state = saslReadyState();
|
|
923
|
-
const ctx = makeCtx(state,
|
|
948
|
+
const ctx = makeCtx(state, services);
|
|
924
949
|
|
|
925
950
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
926
951
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'wrong')), ctx);
|