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.
Files changed (134) hide show
  1. package/.github/workflows/deploy-aws.yml +156 -32
  2. package/.github/workflows/deploy-cf-tcp.yml +11 -9
  3. package/.github/workflows/deploy-cf.yml +14 -14
  4. package/CHANGELOG.md +305 -0
  5. package/README.md +134 -41
  6. package/apps/aws-stack/README.md +3 -5
  7. package/apps/aws-stack/bin/aws.ts +82 -9
  8. package/apps/aws-stack/cdk.json +0 -3
  9. package/apps/aws-stack/package.json +3 -4
  10. package/apps/aws-stack/src/aws-stack.ts +177 -52
  11. package/apps/aws-stack/src/static-site.ts +323 -0
  12. package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
  13. package/apps/aws-stack/tests/stack.test.ts +267 -92
  14. package/apps/aws-stack/tests/static-site.test.ts +491 -0
  15. package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
  16. package/apps/cf-tcp-container/package.json +2 -3
  17. package/apps/cf-tcp-container/src/container-server.ts +12 -9
  18. package/apps/cf-tcp-container/wrangler.toml +1 -10
  19. package/apps/cf-worker/package.json +3 -4
  20. package/apps/cf-worker/wrangler.toml +12 -71
  21. package/apps/local-cli/package.json +1 -1
  22. package/apps/local-cli/src/server.ts +21 -17
  23. package/apps/web/landing/favicon.ico +0 -0
  24. package/apps/web/landing/index.html +1 -0
  25. package/apps/web/package.json +2 -2
  26. package/apps/web/scripts/build.mjs +66 -4
  27. package/apps/web/src/build-env.ts +125 -4
  28. package/apps/web/src/config-schema.ts +20 -6
  29. package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
  30. package/apps/web/tests/build-env.test.ts +210 -9
  31. package/apps/web/tests/build-smoke.test.ts +2 -2
  32. package/apps/web/tests/config-schema.test.ts +149 -25
  33. package/docs/AWS-Deployment.md +670 -96
  34. package/docs/AWS-TCP-Deployment.md +20 -45
  35. package/docs/Cloudflare-Deployment-Guide.md +87 -113
  36. package/docs/Cloudflare-TCP-Deployment.md +25 -49
  37. package/docs/Release-Process.md +27 -23
  38. package/docs/Services.md +69 -22
  39. package/docs/WebClientGuide.md +35 -26
  40. package/package.json +7 -10
  41. package/packages/aws-adapter/package.json +1 -1
  42. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
  43. package/packages/aws-adapter/src/config-loader.ts +19 -2
  44. package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
  45. package/packages/aws-adapter/src/handlers/connect.ts +26 -0
  46. package/packages/aws-adapter/src/handlers/default.ts +190 -123
  47. package/packages/aws-adapter/src/handlers/index.ts +67 -23
  48. package/packages/aws-adapter/src/handlers/nlb-stream.ts +3 -6
  49. package/packages/aws-adapter/src/index.ts +5 -7
  50. package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
  51. package/packages/aws-adapter/src/serialize.ts +15 -0
  52. package/packages/aws-adapter/src/tables.ts +2 -12
  53. package/packages/aws-adapter/tests/aws-harness.ts +0 -1
  54. package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
  55. package/packages/aws-adapter/tests/connect.test.ts +124 -1
  56. package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
  57. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -0
  58. package/packages/aws-adapter/tests/handlers.test.ts +117 -11
  59. package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
  60. package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
  61. package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
  62. package/packages/aws-adapter/tests/stats.test.ts +0 -3
  63. package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
  64. package/packages/aws-adapter/tests/tables.test.ts +1 -8
  65. package/packages/aws-adapter/tests/transactions.test.ts +0 -1
  66. package/packages/cf-adapter/package.json +1 -5
  67. package/packages/cf-adapter/src/cf-runtime.ts +59 -8
  68. package/packages/cf-adapter/src/channel-do.ts +13 -3
  69. package/packages/cf-adapter/src/connection-do.ts +266 -109
  70. package/packages/cf-adapter/src/d1-services-store.ts +63 -26
  71. package/packages/cf-adapter/src/env.ts +11 -10
  72. package/packages/cf-adapter/src/index.ts +0 -6
  73. package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
  74. package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
  75. package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
  76. package/packages/cf-adapter/tests/connection-do-pure.test.ts +95 -54
  77. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
  78. package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
  79. package/packages/cf-adapter/tests/serialize.test.ts +25 -0
  80. package/packages/in-memory-runtime/package.json +1 -1
  81. package/packages/irc-core/package.json +1 -1
  82. package/packages/irc-core/src/account-migration.ts +140 -0
  83. package/packages/irc-core/src/commands/account-auth.ts +16 -19
  84. package/packages/irc-core/src/commands/memoserv.ts +1 -1
  85. package/packages/irc-core/src/commands/nickserv.ts +122 -11
  86. package/packages/irc-core/src/commands/registration.ts +2 -2
  87. package/packages/irc-core/src/commands/sasl.ts +22 -31
  88. package/packages/irc-core/src/config.ts +35 -9
  89. package/packages/irc-core/src/credential-hashing.ts +11 -54
  90. package/packages/irc-core/src/index.ts +1 -0
  91. package/packages/irc-core/src/ports.ts +159 -179
  92. package/packages/irc-core/src/types.ts +38 -10
  93. package/packages/irc-core/tests/account-migration.test.ts +133 -0
  94. package/packages/irc-core/tests/commands/markread.test.ts +54 -0
  95. package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
  96. package/packages/irc-core/tests/commands/nickserv.test.ts +237 -1
  97. package/packages/irc-core/tests/commands/oper.test.ts +15 -0
  98. package/packages/irc-core/tests/commands/registration.test.ts +140 -133
  99. package/packages/irc-core/tests/commands/sasl.test.ts +159 -178
  100. package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
  101. package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
  102. package/packages/irc-core/tests/message-store.test.ts +5 -0
  103. package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
  104. package/packages/irc-core/tests/ports.test.ts +71 -0
  105. package/packages/irc-core/tests/services-store.test.ts +204 -0
  106. package/packages/irc-core/vitest.config.ts +6 -1
  107. package/packages/irc-server/package.json +1 -1
  108. package/packages/irc-server/src/actor.ts +0 -14
  109. package/packages/irc-server/tests/actor.test.ts +19 -47
  110. package/packages/irc-test-support/package.json +1 -1
  111. package/pnpm-workspace.yaml +1 -0
  112. package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
  113. package/scripts/deploy-web-aws.mjs +290 -0
  114. package/scripts/package.json +23 -0
  115. package/scripts/tsconfig.test.json +12 -0
  116. package/scripts/vitest.config.ts +19 -0
  117. package/tools/ci-hardening/package.json +1 -1
  118. package/tools/ci-hardening/src/index.ts +2 -0
  119. package/tools/ci-hardening/src/validate.ts +57 -0
  120. package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
  121. package/tools/ci-hardening/tests/validate.test.ts +42 -0
  122. package/tools/load-test/package.json +1 -1
  123. package/tools/migrate-accounts-to-services.ts +270 -0
  124. package/tools/package.json +2 -1
  125. package/tools/seed-aws-accounts.ts +35 -10
  126. package/tools/seed-cf-accounts.ts +42 -9
  127. package/tools/tcp-ws-forwarder/package.json +1 -1
  128. package/packages/aws-adapter/src/account-store.ts +0 -121
  129. package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
  130. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
  131. package/packages/aws-adapter/tests/account-store.test.ts +0 -276
  132. package/packages/cf-adapter/src/d1-account-store.ts +0 -198
  133. package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
  134. 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 AccountStore accepts the credentials', () => {
133
+ it('completes with 900 and 903 when the ServicesStore accepts the credentials', () => {
139
134
  const state = saslReadyState();
140
- const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
141
- const ctx = makeCtx(state, accounts);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
156
- const ctx = makeCtx(state, accounts);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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, accounts, undefined, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
182
- const services = new InMemoryServicesStore();
175
+ const services = registeredServices('alice', 'secret');
183
176
  services.setLastReadMarker('bob', '#foo', 'm9');
184
- const ctx = makeCtx(state, accounts, undefined, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
187
+ const services = registeredServices('alice', 'secret');
195
188
  const away = new InMemoryAwayStore();
196
189
  away.set('alice', 'brb');
197
- const ctx = makeCtx(state, accounts, undefined, undefined, away);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
200
+ const services = registeredServices('alice', 'secret');
208
201
  const away = new InMemoryAwayStore();
209
202
  away.set('alice', 'brb');
210
- const ctx = makeCtx(state, accounts, undefined, undefined, away);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
216
+ const services = registeredServices('alice', 'secret');
224
217
  const away = new InMemoryAwayStore();
225
- const ctx = makeCtx(state, accounts, undefined, undefined, away);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
238
- const ctx = makeCtx(state, accounts); // no away store
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
243
+ const services = registeredServices('alice', 'secret');
251
244
  const away = new InMemoryAwayStore();
252
245
  away.set('ALICE', 'brb');
253
- const ctx = makeCtx(state, accounts, undefined, undefined, away);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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, accounts, undefined, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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, accounts, undefined, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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, accounts, undefined, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
309
- const services = new InMemoryServicesStore();
298
+ const services = registeredServices('alice', 'secret');
310
299
  services.setVhost('alice', 'cool.example.net');
311
- const ctx = makeCtx(state, accounts, undefined, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
325
- const services = new InMemoryServicesStore();
313
+ const services = registeredServices('alice', 'secret');
326
314
  services.setVhost('alice', 'cool.example.net');
327
- const ctx = makeCtx(state, accounts, undefined, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
339
- const services = new InMemoryServicesStore();
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
352
- const services = new InMemoryServicesStore();
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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, accounts, undefined, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
393
- const ctx = makeCtx(state, accounts);
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 the AccountStore', () => {
385
+ it('forwards parsed PLAIN credentials to services.verifyNick', () => {
402
386
  const state = saslReadyState();
403
- const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
404
- const ctx = makeCtx(state, accounts);
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(accounts.calls).toEqual([
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
417
- const ctx = makeCtx(state, accounts);
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(accounts.calls[0]?.payload).toEqual({
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
434
- const ctx = makeCtx(state, accounts);
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 AccountStore rejects the credentials', () => {
432
+ it('emits 904 ERR_SASLFAIL when the ServicesStore rejects the credentials', () => {
453
433
  const state = saslReadyState();
454
- const accounts = new FakeAccountStore({ ok: false, reason: 'invalid credentials' });
455
- const ctx = makeCtx(state, accounts);
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 accounts = new FakeAccountStore({ ok: false, reason: 'invalid credentials' });
468
- const ctx = makeCtx(state, accounts);
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 accounts = new FakeAccountStore({ ok: false, reason: 'invalid credentials' });
479
- const ctx = makeCtx(state, accounts);
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 AccountStore wired
469
+ // AUTHENTICATE PLAIN — no ServicesStore wired
490
470
  // ============================================================================
491
471
 
492
- describe('authenticateReducer — no AccountStore configured', () => {
493
- it('emits 904 when the PLAIN payload arrives but no AccountStore is wired', () => {
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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, accounts, provider);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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, accounts, provider);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
567
- const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
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 ctx = makeCtx(state, accounts, provider, services);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
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, accounts, provider);
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 account store rejects the identity', () => {
591
+ it('fails with 904 when the subject is not attached to any account', () => {
598
592
  const state = saslReadyState();
599
- const accounts = new FakeAccountStore({ ok: false, reason: 'untrusted certificate' });
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, accounts, provider);
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('fails with 904 when no account store is configured', () => {
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
678
- const ctx = makeCtx(state, accounts);
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 accounts = new FakeAccountStore({ ok: false, reason: 'empty' });
740
- const ctx = makeCtx(state, accounts);
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(accounts.calls).toEqual([]);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
759
- const ctx = makeCtx(state, accounts);
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(accounts.calls).toEqual([]);
789
+ expect(verifyNick).not.toHaveBeenCalled();
775
790
 
776
791
  authenticateReducer(state, authenticate(rest), ctx);
777
- expect(accounts.calls).toEqual([
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
786
- const ctx = makeCtx(state, accounts);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
806
- const ctx = makeCtx(state, accounts);
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(accounts.calls).toEqual([]);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
818
- const ctx = makeCtx(state, accounts);
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(accounts.calls).toEqual([]);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
830
- const ctx = makeCtx(state, accounts);
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(accounts.calls).toEqual([]);
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 accounts = new FakeAccountStore({ ok: true, account: 'alice' });
883
- const ctx = makeCtx(state, accounts);
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 (ServicesStore fallback)
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 when no AccountStore is bound', () => {
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, undefined, undefined, services);
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('succeeds with NickServ credentials even when a static AccountStore is also bound', () => {
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, undefined, undefined, services);
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);