serverless-ircd 0.9.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/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 +305 -0
- package/README.md +134 -41
- 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 +12 -9
- 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 +21 -17
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- 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 +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- 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 +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -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 +3 -6
- 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/config-loader.test.ts +66 -0
- package/packages/aws-adapter/tests/connect.test.ts +124 -1
- 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/handlers.test.ts +117 -11
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- 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 +0 -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 +266 -109
- 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 +95 -54
- 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 +16 -19
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +122 -11
- package/packages/irc-core/src/commands/registration.ts +2 -2
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- 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/types.ts +38 -10
- package/packages/irc-core/tests/account-migration.test.ts +133 -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 +237 -1
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +140 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +159 -178
- 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 +0 -14
- package/packages/irc-server/tests/actor.test.ts +19 -47
- package/packages/irc-test-support/package.json +1 -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/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/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);
|
|
@@ -305,10 +295,9 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
305
295
|
it('applies the assigned HostServ vhost on SASL PLAIN success', () => {
|
|
306
296
|
const state = saslReadyState();
|
|
307
297
|
state.host = 'real.example.net';
|
|
308
|
-
const
|
|
309
|
-
const services = new InMemoryServicesStore();
|
|
298
|
+
const services = registeredServices('alice', 'secret');
|
|
310
299
|
services.setVhost('alice', 'cool.example.net');
|
|
311
|
-
const ctx = makeCtx(state,
|
|
300
|
+
const ctx = makeCtx(state, services);
|
|
312
301
|
|
|
313
302
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
314
303
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -321,10 +310,9 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
321
310
|
it('does not emit a CHGHOST broadcast on SASL success (pre-registration, no peers)', () => {
|
|
322
311
|
const state = saslReadyState();
|
|
323
312
|
state.host = 'real.example.net';
|
|
324
|
-
const
|
|
325
|
-
const services = new InMemoryServicesStore();
|
|
313
|
+
const services = registeredServices('alice', 'secret');
|
|
326
314
|
services.setVhost('alice', 'cool.example.net');
|
|
327
|
-
const ctx = makeCtx(state,
|
|
315
|
+
const ctx = makeCtx(state, services);
|
|
328
316
|
|
|
329
317
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
330
318
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -335,9 +323,8 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
335
323
|
it('leaves host unchanged on SASL success when no vhost is assigned', () => {
|
|
336
324
|
const state = saslReadyState();
|
|
337
325
|
state.host = 'real.example.net';
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
const ctx = makeCtx(state, accounts, undefined, services);
|
|
326
|
+
const services = registeredServices('alice', 'secret');
|
|
327
|
+
const ctx = makeCtx(state, services);
|
|
341
328
|
|
|
342
329
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
343
330
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -348,9 +335,8 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
348
335
|
|
|
349
336
|
it('delivers nothing on a SASL login when the account has no memos', () => {
|
|
350
337
|
const state = saslReadyState();
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
const ctx = makeCtx(state, accounts, undefined, services);
|
|
338
|
+
const services = registeredServices('alice', 'secret');
|
|
339
|
+
const ctx = makeCtx(state, services);
|
|
354
340
|
|
|
355
341
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
356
342
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -361,10 +347,9 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
361
347
|
|
|
362
348
|
it('looks up queued memos case-insensitively on a SASL login', () => {
|
|
363
349
|
const state = saslReadyState();
|
|
364
|
-
const
|
|
365
|
-
const services = new InMemoryServicesStore();
|
|
350
|
+
const services = registeredServices('alice', 'secret');
|
|
366
351
|
services.recordMemo({ from: 'bob', to: 'ALICE', body: 'folded', sentAt: 1_000 });
|
|
367
|
-
const ctx = makeCtx(state,
|
|
352
|
+
const ctx = makeCtx(state, services);
|
|
368
353
|
|
|
369
354
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
370
355
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -377,8 +362,7 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
377
362
|
|
|
378
363
|
it('does not deliver memos when no ServicesStore is bound', () => {
|
|
379
364
|
const state = saslReadyState();
|
|
380
|
-
const
|
|
381
|
-
const ctx = makeCtx(state, accounts);
|
|
365
|
+
const ctx = makeCtx(state);
|
|
382
366
|
|
|
383
367
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
384
368
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -389,8 +373,8 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
389
373
|
|
|
390
374
|
it('clears the in-progress mechanism after a successful authentication', () => {
|
|
391
375
|
const state = saslReadyState();
|
|
392
|
-
const
|
|
393
|
-
const ctx = makeCtx(state,
|
|
376
|
+
const services = registeredServices('alice', 'secret');
|
|
377
|
+
const ctx = makeCtx(state, services);
|
|
394
378
|
|
|
395
379
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
396
380
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -398,40 +382,36 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
398
382
|
expect(state.saslMech).toBeUndefined();
|
|
399
383
|
});
|
|
400
384
|
|
|
401
|
-
it('forwards parsed PLAIN credentials to
|
|
385
|
+
it('forwards parsed PLAIN credentials to services.verifyNick', () => {
|
|
402
386
|
const state = saslReadyState();
|
|
403
|
-
const
|
|
404
|
-
const
|
|
387
|
+
const services = registeredServices('alice', 'secret');
|
|
388
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
389
|
+
const ctx = makeCtx(state, services);
|
|
405
390
|
|
|
406
391
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
407
392
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
408
393
|
|
|
409
|
-
expect(
|
|
410
|
-
{ mech: 'PLAIN', payload: { kind: 'PLAIN', username: 'alice', password: 'secret' } },
|
|
411
|
-
]);
|
|
394
|
+
expect(verifyNick).toHaveBeenCalledWith('alice', 'secret');
|
|
412
395
|
});
|
|
413
396
|
|
|
414
397
|
it('ignores the authorization identity field in the PLAIN payload', () => {
|
|
415
398
|
const state = saslReadyState();
|
|
416
|
-
const
|
|
417
|
-
const
|
|
399
|
+
const services = registeredServices('alice', 'secret');
|
|
400
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
401
|
+
const ctx = makeCtx(state, services);
|
|
418
402
|
|
|
419
403
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
420
404
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret', 'bob')), ctx);
|
|
421
405
|
|
|
422
|
-
expect(
|
|
423
|
-
kind: 'PLAIN',
|
|
424
|
-
username: 'alice',
|
|
425
|
-
password: 'secret',
|
|
426
|
-
});
|
|
406
|
+
expect(verifyNick).toHaveBeenCalledWith('alice', 'secret');
|
|
427
407
|
});
|
|
428
408
|
|
|
429
409
|
it('targets `*` with an empty hostmask when the client has no nick yet', () => {
|
|
430
410
|
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
431
411
|
state.capNegotiating = true;
|
|
432
412
|
state.caps.add('sasl');
|
|
433
|
-
const
|
|
434
|
-
const ctx = makeCtx(state,
|
|
413
|
+
const services = registeredServices('alice', 'secret');
|
|
414
|
+
const ctx = makeCtx(state, services);
|
|
435
415
|
|
|
436
416
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
437
417
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -449,10 +429,10 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
449
429
|
// ============================================================================
|
|
450
430
|
|
|
451
431
|
describe('authenticateReducer — PLAIN failure', () => {
|
|
452
|
-
it('emits 904 ERR_SASLFAIL when the
|
|
432
|
+
it('emits 904 ERR_SASLFAIL when the ServicesStore rejects the credentials', () => {
|
|
453
433
|
const state = saslReadyState();
|
|
454
|
-
const
|
|
455
|
-
const ctx = makeCtx(state,
|
|
434
|
+
const services = registeredServices('alice', 'secret');
|
|
435
|
+
const ctx = makeCtx(state, services);
|
|
456
436
|
|
|
457
437
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
458
438
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'wrong')), ctx);
|
|
@@ -464,8 +444,8 @@ describe('authenticateReducer — PLAIN failure', () => {
|
|
|
464
444
|
|
|
465
445
|
it('does not record an account on failure', () => {
|
|
466
446
|
const state = saslReadyState();
|
|
467
|
-
const
|
|
468
|
-
const ctx = makeCtx(state,
|
|
447
|
+
const services = registeredServices('alice', 'secret');
|
|
448
|
+
const ctx = makeCtx(state, services);
|
|
469
449
|
|
|
470
450
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
471
451
|
authenticateReducer(state, authenticate(plainPayload('alice', 'wrong')), ctx);
|
|
@@ -475,8 +455,8 @@ describe('authenticateReducer — PLAIN failure', () => {
|
|
|
475
455
|
|
|
476
456
|
it('clears the in-progress mechanism after a failure', () => {
|
|
477
457
|
const state = saslReadyState();
|
|
478
|
-
const
|
|
479
|
-
const ctx = makeCtx(state,
|
|
458
|
+
const services = registeredServices('alice', 'secret');
|
|
459
|
+
const ctx = makeCtx(state, services);
|
|
480
460
|
|
|
481
461
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
482
462
|
authenticateReducer(state, authenticate(plainPayload('alice', 'wrong')), ctx);
|
|
@@ -486,11 +466,11 @@ describe('authenticateReducer — PLAIN failure', () => {
|
|
|
486
466
|
});
|
|
487
467
|
|
|
488
468
|
// ============================================================================
|
|
489
|
-
// AUTHENTICATE PLAIN — no
|
|
469
|
+
// AUTHENTICATE PLAIN — no ServicesStore wired
|
|
490
470
|
// ============================================================================
|
|
491
471
|
|
|
492
|
-
describe('authenticateReducer — no
|
|
493
|
-
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', () => {
|
|
494
474
|
const state = saslReadyState();
|
|
495
475
|
const ctx = makeCtx(state);
|
|
496
476
|
|
|
@@ -531,28 +511,41 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
531
511
|
|
|
532
512
|
it('succeeds (900/903) when a valid cert identity is presented', () => {
|
|
533
513
|
const state = saslReadyState();
|
|
534
|
-
const
|
|
514
|
+
const services = registeredServices('alice', 'secret');
|
|
515
|
+
services.addCertFP('alice', 'CN=alice');
|
|
535
516
|
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
536
|
-
const ctx = makeCtx(state,
|
|
517
|
+
const ctx = makeCtx(state, services, provider);
|
|
537
518
|
|
|
538
519
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
539
520
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
540
521
|
|
|
541
522
|
expect(state.account).toBe('alice');
|
|
542
|
-
expect(accounts.calls).toEqual([
|
|
543
|
-
{ mech: 'EXTERNAL', payload: { kind: 'EXTERNAL', identity: 'CN=alice' } },
|
|
544
|
-
]);
|
|
545
523
|
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
546
524
|
L(`:${SV} 900 alice alice!alice@example.com alice :You are now logged in as alice`),
|
|
547
525
|
L(`:${SV} 903 alice :SASL authentication successful`),
|
|
548
526
|
]);
|
|
549
527
|
});
|
|
550
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
|
+
|
|
551
543
|
it('clears saslMech/saslBuffer after a successful EXTERNAL', () => {
|
|
552
544
|
const state = saslReadyState();
|
|
553
|
-
const
|
|
545
|
+
const services = registeredServices('alice', 'secret');
|
|
546
|
+
services.addCertFP('alice', 'CN=alice');
|
|
554
547
|
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
555
|
-
const ctx = makeCtx(state,
|
|
548
|
+
const ctx = makeCtx(state, services, provider);
|
|
556
549
|
|
|
557
550
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
558
551
|
authenticateReducer(state, authenticate('+'), ctx);
|
|
@@ -563,11 +556,11 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
563
556
|
|
|
564
557
|
it('delivers queued memos on a successful EXTERNAL login', () => {
|
|
565
558
|
const state = saslReadyState();
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
const services = new InMemoryServicesStore();
|
|
559
|
+
const services = registeredServices('alice', 'secret');
|
|
560
|
+
services.addCertFP('alice', 'CN=alice');
|
|
569
561
|
services.recordMemo({ from: 'bob', to: 'alice', body: 'via mTLS', sentAt: 1_000 });
|
|
570
|
-
const
|
|
562
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
563
|
+
const ctx = makeCtx(state, services, provider);
|
|
571
564
|
|
|
572
565
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
573
566
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
@@ -581,9 +574,10 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
581
574
|
|
|
582
575
|
it('fails with 904 when the provider has no identity for the connection', () => {
|
|
583
576
|
const state = saslReadyState();
|
|
584
|
-
const
|
|
577
|
+
const services = registeredServices('alice', 'secret');
|
|
578
|
+
services.addCertFP('alice', 'CN=alice');
|
|
585
579
|
const provider = new FakeMtlsIdentityProvider('c1', undefined);
|
|
586
|
-
const ctx = makeCtx(state,
|
|
580
|
+
const ctx = makeCtx(state, services, provider);
|
|
587
581
|
|
|
588
582
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
589
583
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
@@ -594,11 +588,12 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
594
588
|
expect(state.account).toBeUndefined();
|
|
595
589
|
});
|
|
596
590
|
|
|
597
|
-
it('fails with 904 when the
|
|
591
|
+
it('fails with 904 when the subject is not attached to any account', () => {
|
|
598
592
|
const state = saslReadyState();
|
|
599
|
-
const
|
|
593
|
+
const services = registeredServices('alice', 'secret');
|
|
594
|
+
// No cert subject attached to alice; CN=evil is unknown to the store.
|
|
600
595
|
const provider = new FakeMtlsIdentityProvider('c1', 'CN=evil');
|
|
601
|
-
const ctx = makeCtx(state,
|
|
596
|
+
const ctx = makeCtx(state, services, provider);
|
|
602
597
|
|
|
603
598
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
604
599
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
@@ -606,9 +601,27 @@ describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
|
606
601
|
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
607
602
|
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
608
603
|
]);
|
|
604
|
+
expect(state.account).toBeUndefined();
|
|
609
605
|
});
|
|
610
606
|
|
|
611
|
-
it('
|
|
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();
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('fails with 904 when no ServicesStore is configured', () => {
|
|
612
625
|
const state = saslReadyState();
|
|
613
626
|
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
614
627
|
const ctx = makeCtx(state, undefined, provider);
|
|
@@ -674,8 +687,8 @@ describe('authenticateReducer — abort / already-authenticated', () => {
|
|
|
674
687
|
|
|
675
688
|
it('emits 907 when sent after a successful SASL authentication', () => {
|
|
676
689
|
const state = saslReadyState();
|
|
677
|
-
const
|
|
678
|
-
const ctx = makeCtx(state,
|
|
690
|
+
const services = registeredServices('alice', 'secret');
|
|
691
|
+
const ctx = makeCtx(state, services);
|
|
679
692
|
|
|
680
693
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
681
694
|
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
@@ -736,13 +749,14 @@ describe('authenticateReducer — client abort', () => {
|
|
|
736
749
|
describe('authenticateReducer — empty payload (`+`)', () => {
|
|
737
750
|
it('treats `AUTHENTICATE +` as the final (empty) chunk and rejects malformed PLAIN', () => {
|
|
738
751
|
const state = saslReadyState();
|
|
739
|
-
const
|
|
740
|
-
const
|
|
752
|
+
const services = registeredServices('alice', 'secret');
|
|
753
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
754
|
+
const ctx = makeCtx(state, services);
|
|
741
755
|
|
|
742
756
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
743
757
|
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
744
758
|
|
|
745
|
-
expect(
|
|
759
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
746
760
|
expect(firstLineText(out.effects[0])).toContain(' 904 ');
|
|
747
761
|
expect(state.saslMech).toBeUndefined();
|
|
748
762
|
});
|
|
@@ -755,8 +769,9 @@ describe('authenticateReducer — empty payload (`+`)', () => {
|
|
|
755
769
|
describe('authenticateReducer — chunked payload', () => {
|
|
756
770
|
it('buffers chunks of exactly 400 bytes and verifies on the short final chunk', () => {
|
|
757
771
|
const state = saslReadyState();
|
|
758
|
-
const
|
|
759
|
-
const
|
|
772
|
+
const services = registeredServices('alice', 'p'.repeat(320));
|
|
773
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
774
|
+
const ctx = makeCtx(state, services);
|
|
760
775
|
|
|
761
776
|
const longPassword = 'p'.repeat(320);
|
|
762
777
|
const payload = plainPayload('alice', longPassword);
|
|
@@ -771,19 +786,17 @@ describe('authenticateReducer — chunked payload', () => {
|
|
|
771
786
|
const buffered = authenticateReducer(state, authenticate(first), ctx);
|
|
772
787
|
expect(buffered.effects).toEqual([]);
|
|
773
788
|
expect(state.saslMech).toBe('PLAIN');
|
|
774
|
-
expect(
|
|
789
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
775
790
|
|
|
776
791
|
authenticateReducer(state, authenticate(rest), ctx);
|
|
777
|
-
expect(
|
|
778
|
-
{ mech: 'PLAIN', payload: { kind: 'PLAIN', username: 'alice', password: longPassword } },
|
|
779
|
-
]);
|
|
792
|
+
expect(verifyNick).toHaveBeenCalledWith('alice', longPassword);
|
|
780
793
|
expect(state.account).toBe('alice');
|
|
781
794
|
});
|
|
782
795
|
|
|
783
796
|
it('rejects an oversized payload with 905 ERR_SASLTOOLONG', () => {
|
|
784
797
|
const state = saslReadyState();
|
|
785
|
-
const
|
|
786
|
-
const ctx = makeCtx(state,
|
|
798
|
+
const services = registeredServices('alice', 'secret');
|
|
799
|
+
const ctx = makeCtx(state, services);
|
|
787
800
|
|
|
788
801
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
789
802
|
const out = authenticateReducer(state, authenticate(`${'A'.repeat(8193)}`), ctx);
|
|
@@ -802,37 +815,40 @@ describe('authenticateReducer — chunked payload', () => {
|
|
|
802
815
|
describe('authenticateReducer — malformed PLAIN', () => {
|
|
803
816
|
it('emits 904 when the base64 payload does not decode to three NUL-separated fields', () => {
|
|
804
817
|
const state = saslReadyState();
|
|
805
|
-
const
|
|
806
|
-
const
|
|
818
|
+
const services = registeredServices('alice', 'secret');
|
|
819
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
820
|
+
const ctx = makeCtx(state, services);
|
|
807
821
|
|
|
808
822
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
809
823
|
const out = authenticateReducer(state, authenticate(encodeBase64('onlyonefield')), ctx);
|
|
810
824
|
|
|
811
825
|
expect(firstLineText(out.effects[0])).toContain(' 904 ');
|
|
812
|
-
expect(
|
|
826
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
813
827
|
});
|
|
814
828
|
|
|
815
829
|
it('emits 904 when the payload is not valid base64', () => {
|
|
816
830
|
const state = saslReadyState();
|
|
817
|
-
const
|
|
818
|
-
const
|
|
831
|
+
const services = registeredServices('alice', 'secret');
|
|
832
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
833
|
+
const ctx = makeCtx(state, services);
|
|
819
834
|
|
|
820
835
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
821
836
|
const out = authenticateReducer(state, authenticate('!!!notbase64!!!'), ctx);
|
|
822
837
|
|
|
823
838
|
expect(firstLineText(out.effects[0])).toContain(' 904 ');
|
|
824
|
-
expect(
|
|
839
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
825
840
|
});
|
|
826
841
|
|
|
827
842
|
it('emits 908 when the first AUTHENTICATE frame is not a recognized mechanism', () => {
|
|
828
843
|
const state = saslReadyState();
|
|
829
|
-
const
|
|
830
|
-
const
|
|
844
|
+
const services = registeredServices('alice', 'secret');
|
|
845
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
846
|
+
const ctx = makeCtx(state, services);
|
|
831
847
|
|
|
832
848
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
833
849
|
|
|
834
850
|
expect(firstLineText(out.effects[0])).toContain(' 908 ');
|
|
835
|
-
expect(
|
|
851
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
836
852
|
});
|
|
837
853
|
});
|
|
838
854
|
|
|
@@ -879,8 +895,8 @@ describe('authenticateReducer — CAP REQ sasl integration', () => {
|
|
|
879
895
|
state.user = 'alice';
|
|
880
896
|
state.host = 'example.com';
|
|
881
897
|
state.registration = 'registering';
|
|
882
|
-
const
|
|
883
|
-
const ctx = makeCtx(state,
|
|
898
|
+
const services = registeredServices('alice', 'secret');
|
|
899
|
+
const ctx = makeCtx(state, services);
|
|
884
900
|
|
|
885
901
|
capReducer(state, { command: 'CAP', params: ['REQ', 'sasl'], tags: {} }, ctx);
|
|
886
902
|
expect(state.caps.has('sasl')).toBe(true);
|
|
@@ -905,15 +921,15 @@ describe('authenticateReducer — CAP REQ sasl integration', () => {
|
|
|
905
921
|
});
|
|
906
922
|
|
|
907
923
|
// ============================================================================
|
|
908
|
-
// SASL PLAIN — NickServ-registered accounts (
|
|
924
|
+
// SASL PLAIN — NickServ-registered accounts (single credential home)
|
|
909
925
|
// ============================================================================
|
|
910
926
|
|
|
911
927
|
describe('authenticateReducer — NickServ-registered nick via ServicesStore', () => {
|
|
912
|
-
it('succeeds with credentials registered via NickServ
|
|
928
|
+
it('succeeds with credentials registered via NickServ (no second registration)', () => {
|
|
913
929
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
914
930
|
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
915
931
|
const state = saslReadyState();
|
|
916
|
-
const ctx = makeCtx(state,
|
|
932
|
+
const ctx = makeCtx(state, services);
|
|
917
933
|
|
|
918
934
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
919
935
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'hunter2')), ctx);
|
|
@@ -925,46 +941,11 @@ describe('authenticateReducer — NickServ-registered nick via ServicesStore', (
|
|
|
925
941
|
);
|
|
926
942
|
});
|
|
927
943
|
|
|
928
|
-
it('
|
|
929
|
-
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
930
|
-
services.registerNick('bob', 'bobpass', 'bob@example.com');
|
|
931
|
-
// Static AccountStore does not know about bob; the services store does.
|
|
932
|
-
const accounts = new FakeAccountStore({ ok: false, reason: 'invalid credentials' });
|
|
933
|
-
const state = saslReadyState();
|
|
934
|
-
state.nick = 'bob';
|
|
935
|
-
state.user = 'bob';
|
|
936
|
-
state.host = 'example.com';
|
|
937
|
-
const ctx = makeCtx(state, accounts, undefined, services);
|
|
938
|
-
|
|
939
|
-
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
940
|
-
const out = authenticateReducer(state, authenticate(plainPayload('bob', 'bobpass')), ctx);
|
|
941
|
-
|
|
942
|
-
expect(state.account).toBe('bob');
|
|
943
|
-
expect(sendLinesOf(out.effects[0]).map((l) => l.text)).toContain(
|
|
944
|
-
`:${SV} 903 bob :SASL authentication successful`,
|
|
945
|
-
);
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
it('prefers the static AccountStore over the services store on a match', () => {
|
|
949
|
-
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
950
|
-
services.registerNick('alice', 'servicespw', 'alice@example.com');
|
|
951
|
-
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
952
|
-
const state = saslReadyState();
|
|
953
|
-
const ctx = makeCtx(state, accounts, undefined, services);
|
|
954
|
-
|
|
955
|
-
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
956
|
-
authenticateReducer(state, authenticate(plainPayload('alice', 'staticpw')), ctx);
|
|
957
|
-
|
|
958
|
-
// The static store vouched for the account; it is used.
|
|
959
|
-
expect(accounts.calls).toHaveLength(1);
|
|
960
|
-
expect(state.account).toBe('alice');
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
it('fails when neither the AccountStore nor the ServicesStore verifies the credential', () => {
|
|
944
|
+
it('fails when neither the nick nor a matching credential verifies', () => {
|
|
964
945
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
965
946
|
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
966
947
|
const state = saslReadyState();
|
|
967
|
-
const ctx = makeCtx(state,
|
|
948
|
+
const ctx = makeCtx(state, services);
|
|
968
949
|
|
|
969
950
|
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
970
951
|
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'wrong')), ctx);
|