serverless-ircd 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +4 -0
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +11 -9
- package/.github/workflows/deploy-cf.yml +14 -14
- package/CHANGELOG.md +550 -0
- package/README.md +275 -222
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +82 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +177 -52
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +267 -92
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/package.json +2 -3
- package/apps/cf-tcp-container/src/container-server.ts +33 -10
- package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
- package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
- package/apps/cf-tcp-container/wrangler.toml +1 -10
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/wrangler.toml +12 -71
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +115 -48
- package/apps/local-cli/tests/config-resolution.test.ts +65 -0
- package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
- package/apps/local-cli/tests/rehash.test.ts +147 -0
- package/apps/local-cli/tests/server-helpers.test.ts +63 -0
- package/apps/local-cli/tests/tcp.test.ts +89 -0
- package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +227 -3
- package/apps/web/package.json +3 -2
- package/apps/web/scripts/build.mjs +91 -6
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/src/render-docs.ts +292 -0
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +33 -4
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/apps/web/tests/landing-content.test.ts +103 -0
- package/apps/web/tests/render-docs.test.ts +198 -0
- package/docs/AWS-Adapter-Architecture.md +3 -2
- package/docs/AWS-Deployment.md +670 -96
- package/docs/AWS-TCP-Deployment.md +20 -45
- package/docs/Cloudflare-Deployment-Guide.md +87 -113
- package/docs/Cloudflare-TCP-Deployment.md +25 -49
- package/docs/Release-Process.md +27 -23
- package/docs/Services.md +102 -23
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +15 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
- package/packages/aws-adapter/src/config-loader.ts +19 -2
- package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
- package/packages/aws-adapter/src/handlers/connect.ts +26 -0
- package/packages/aws-adapter/src/handlers/default.ts +190 -123
- package/packages/aws-adapter/src/handlers/index.ts +67 -23
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +13 -8
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +15 -0
- package/packages/aws-adapter/src/tables.ts +2 -12
- package/packages/aws-adapter/tests/aws-harness.ts +0 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
- package/packages/aws-adapter/tests/connect.test.ts +124 -1
- package/packages/aws-adapter/tests/connection-counter.test.ts +17 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -0
- package/packages/aws-adapter/tests/global-setup.ts +28 -1
- package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
- package/packages/aws-adapter/tests/handlers.test.ts +117 -11
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +20 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +59 -8
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/connection-do.ts +284 -115
- package/packages/cf-adapter/src/d1-services-store.ts +63 -26
- package/packages/cf-adapter/src/env.ts +11 -10
- package/packages/cf-adapter/src/index.ts +0 -6
- package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +222 -51
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/commands/account-auth.ts +60 -35
- package/packages/irc-core/src/commands/chanserv.ts +288 -4
- package/packages/irc-core/src/commands/hostserv.ts +38 -3
- package/packages/irc-core/src/commands/index.ts +1 -0
- package/packages/irc-core/src/commands/join.ts +41 -35
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +138 -15
- package/packages/irc-core/src/commands/registration.ts +28 -17
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/topic.ts +23 -10
- package/packages/irc-core/src/config.ts +35 -9
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +159 -179
- package/packages/irc-core/src/state/channel.ts +17 -0
- package/packages/irc-core/src/types.ts +38 -10
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/commands/chanserv.test.ts +668 -1
- package/packages/irc-core/tests/commands/hostserv.test.ts +71 -0
- package/packages/irc-core/tests/commands/join.test.ts +179 -0
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +422 -3
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +336 -108
- package/packages/irc-core/tests/commands/sasl.test.ts +194 -169
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
- package/packages/irc-core/tests/ports.test.ts +71 -0
- package/packages/irc-core/tests/services-store.test.ts +204 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +80 -44
- package/packages/irc-server/tests/actor.test.ts +384 -50
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +8 -5
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +19 -0
- package/packages/irc-test-support/vitest.config.ts +6 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/src/index.ts +2 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/load-test/package.json +1 -1
- package/tools/load-test/src/client.ts +13 -13
- package/tools/load-test/tests/client.test.ts +258 -2
- package/tools/load-test/tests/config.test.ts +39 -0
- package/tools/load-test/tests/harness.test.ts +21 -0
- package/tools/load-test/tests/metrics.test.ts +7 -0
- package/tools/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
- package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +51 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -283,6 +283,144 @@ describe('nickservReducer — IDENTIFY', () => {
|
|
|
283
283
|
});
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
describe('nickservReducer — ID (alias for IDENTIFY)', () => {
|
|
287
|
+
it('sets +r and account on correct password (ID <password>)', () => {
|
|
288
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
289
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
290
|
+
const conn = makeConn();
|
|
291
|
+
const ctx = makeCtx(conn, services);
|
|
292
|
+
|
|
293
|
+
const out = nickservReducer(conn, privmsg('ID hunter2'), ctx);
|
|
294
|
+
|
|
295
|
+
expect(conn.account).toBe('alice');
|
|
296
|
+
expect(conn.userModes.registered).toBe(true);
|
|
297
|
+
expect(sentTexts(out.effects)).toContain(
|
|
298
|
+
':NickServ!NickServ@services NOTICE alice :You are now identified for nick alice.',
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('is case-insensitive on the subcommand (id)', () => {
|
|
303
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
304
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
305
|
+
const conn = makeConn();
|
|
306
|
+
const ctx = makeCtx(conn, services);
|
|
307
|
+
|
|
308
|
+
nickservReducer(conn, privmsg('id hunter2'), ctx);
|
|
309
|
+
|
|
310
|
+
expect(conn.account).toBe('alice');
|
|
311
|
+
expect(conn.userModes.registered).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('rejects ID with wrong password (no +r, NOTICE only)', () => {
|
|
315
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
316
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
317
|
+
const conn = makeConn();
|
|
318
|
+
const ctx = makeCtx(conn, services);
|
|
319
|
+
|
|
320
|
+
const out = nickservReducer(conn, privmsg('ID wrongpw'), ctx);
|
|
321
|
+
|
|
322
|
+
expect(conn.account).toBeUndefined();
|
|
323
|
+
expect(conn.userModes.registered).toBe(false);
|
|
324
|
+
expect(sentTexts(out.effects)).toContain(
|
|
325
|
+
':NickServ!NickServ@services NOTICE alice :Invalid password.',
|
|
326
|
+
);
|
|
327
|
+
expect(out.effects.filter((e) => e.tag === 'Broadcast')).toHaveLength(0);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('sets +r and account for ID <nick> <password>', () => {
|
|
331
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
332
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
333
|
+
// Connection is using nick "bob" but identifies as "alice".
|
|
334
|
+
const conn = makeConn('c1', 'bob');
|
|
335
|
+
const ctx = makeCtx(conn, services);
|
|
336
|
+
|
|
337
|
+
const out = nickservReducer(conn, privmsg('ID alice hunter2'), ctx);
|
|
338
|
+
|
|
339
|
+
expect(conn.account).toBe('alice');
|
|
340
|
+
expect(conn.userModes.registered).toBe(true);
|
|
341
|
+
expect(sentTexts(out.effects)).toContain(
|
|
342
|
+
':NickServ!NickServ@services NOTICE bob :You are now identified for nick alice.',
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// IDENTIFY — HostServ vhost auto-apply on identify
|
|
349
|
+
// ============================================================================
|
|
350
|
+
|
|
351
|
+
describe('nickservReducer — IDENTIFY applies HostServ vhost', () => {
|
|
352
|
+
it('applies an assigned vhost on a successful IDENTIFY', () => {
|
|
353
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
354
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
355
|
+
services.setVhost('alice', 'cool.example.net');
|
|
356
|
+
const conn = makeConn();
|
|
357
|
+
conn.host = 'real.example.net';
|
|
358
|
+
const ctx = makeCtx(conn, services);
|
|
359
|
+
|
|
360
|
+
nickservReducer(conn, privmsg('IDENTIFY hunter2'), ctx);
|
|
361
|
+
|
|
362
|
+
expect(conn.account).toBe('alice');
|
|
363
|
+
expect(conn.vhostActive).toBe(true);
|
|
364
|
+
expect(conn.host).toBe('cool.example.net');
|
|
365
|
+
expect(conn.preVhostHost).toBe('real.example.net');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('leaves the host unchanged on IDENTIFY when no vhost is assigned', () => {
|
|
369
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
370
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
371
|
+
const conn = makeConn();
|
|
372
|
+
conn.host = 'real.example.net';
|
|
373
|
+
const ctx = makeCtx(conn, services);
|
|
374
|
+
|
|
375
|
+
nickservReducer(conn, privmsg('IDENTIFY hunter2'), ctx);
|
|
376
|
+
|
|
377
|
+
expect(conn.vhostActive).toBeUndefined();
|
|
378
|
+
expect(conn.host).toBe('real.example.net');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('emits a CHGHOST broadcast to joined peers on a post-JOIN IDENTIFY', () => {
|
|
382
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
383
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
384
|
+
services.setVhost('alice', 'cool.example.net');
|
|
385
|
+
const conn = makeConn();
|
|
386
|
+
conn.user = 'aliceuser';
|
|
387
|
+
conn.host = 'real.example.net';
|
|
388
|
+
conn.joinedChannels.add('#foo');
|
|
389
|
+
const ctx = makeCtx(conn, services);
|
|
390
|
+
|
|
391
|
+
const out = nickservReducer(conn, privmsg('IDENTIFY hunter2'), ctx);
|
|
392
|
+
|
|
393
|
+
const chghost = out.effects.filter((e) => e.tag === 'Broadcast' && e.cap === 'chghost');
|
|
394
|
+
expect(chghost).toHaveLength(1);
|
|
395
|
+
if (chghost[0]?.tag === 'Broadcast') {
|
|
396
|
+
expect(chghost[0].chan).toBe('#foo');
|
|
397
|
+
expect(chghost[0].capLines?.[0]?.text).toBe(
|
|
398
|
+
':alice!aliceuser@real.example.net CHGHOST aliceuser :cool.example.net',
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('still emits the NickServ confirmation NOTICE and ACCOUNT broadcast alongside the vhost apply', () => {
|
|
404
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
405
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
406
|
+
services.setVhost('alice', 'cool.example.net');
|
|
407
|
+
const conn = makeConn();
|
|
408
|
+
conn.host = 'real.example.net';
|
|
409
|
+
conn.joinedChannels.add('#foo');
|
|
410
|
+
const ctx = makeCtx(conn, services);
|
|
411
|
+
|
|
412
|
+
const out = nickservReducer(conn, privmsg('IDENTIFY hunter2'), ctx);
|
|
413
|
+
|
|
414
|
+
expect(sentTexts(out.effects)).toContain(
|
|
415
|
+
':NickServ!NickServ@services NOTICE alice :You are now identified for nick alice.',
|
|
416
|
+
);
|
|
417
|
+
const accountBcast = out.effects.find(
|
|
418
|
+
(e) => e.tag === 'Broadcast' && e.cap === 'account-notify',
|
|
419
|
+
);
|
|
420
|
+
expect(accountBcast).toBeDefined();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
286
424
|
describe('nickservReducer — DROP', () => {
|
|
287
425
|
it('removes the registration when the connection is identified', () => {
|
|
288
426
|
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
@@ -364,10 +502,11 @@ describe('nickservReducer — DROP', () => {
|
|
|
364
502
|
});
|
|
365
503
|
|
|
366
504
|
describe('nickservReducer — INFO', () => {
|
|
367
|
-
it('
|
|
505
|
+
it('shows the full record (incl. Email) to the owner via INFO with no target', () => {
|
|
368
506
|
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
369
507
|
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
370
508
|
const conn = makeConn();
|
|
509
|
+
conn.account = 'alice';
|
|
371
510
|
const ctx = makeCtx(conn, services);
|
|
372
511
|
|
|
373
512
|
const out = nickservReducer(conn, privmsg('INFO'), ctx);
|
|
@@ -378,10 +517,54 @@ describe('nickservReducer — INFO', () => {
|
|
|
378
517
|
expect(texts).toContain(':NickServ!NickServ@services NOTICE alice :Email: alice@example.com');
|
|
379
518
|
});
|
|
380
519
|
|
|
381
|
-
it('
|
|
520
|
+
it('withholds Email from a non-owner querying someone else nick', () => {
|
|
521
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
522
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
523
|
+
const conn = makeConn();
|
|
524
|
+
const ctx = makeCtx(conn, services);
|
|
525
|
+
|
|
526
|
+
const out = nickservReducer(conn, privmsg('INFO bob'), ctx);
|
|
527
|
+
|
|
528
|
+
const texts = sentTexts(out.effects);
|
|
529
|
+
expect(texts).toContain(':NickServ!NickServ@services NOTICE alice :Nick: bob');
|
|
530
|
+
expect(texts).toContain(':NickServ!NickServ@services NOTICE alice :Account: bob');
|
|
531
|
+
expect(texts).not.toContain(':NickServ!NickServ@services NOTICE alice :Email: bob@example.com');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('withholds Email from a non-owner querying their own nick when unidentified', () => {
|
|
535
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
536
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
537
|
+
const conn = makeConn();
|
|
538
|
+
const ctx = makeCtx(conn, services);
|
|
539
|
+
|
|
540
|
+
const out = nickservReducer(conn, privmsg('INFO'), ctx);
|
|
541
|
+
|
|
542
|
+
const texts = sentTexts(out.effects);
|
|
543
|
+
expect(texts).toContain(':NickServ!NickServ@services NOTICE alice :Nick: alice');
|
|
544
|
+
expect(texts).not.toContain(
|
|
545
|
+
':NickServ!NickServ@services NOTICE alice :Email: alice@example.com',
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('shows Email to the owner querying their own nick by name', () => {
|
|
550
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
551
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
552
|
+
const conn = makeConn('c1', 'bob');
|
|
553
|
+
conn.account = 'bob';
|
|
554
|
+
const ctx = makeCtx(conn, services);
|
|
555
|
+
|
|
556
|
+
const out = nickservReducer(conn, privmsg('INFO bob'), ctx);
|
|
557
|
+
|
|
558
|
+
const texts = sentTexts(out.effects);
|
|
559
|
+
expect(texts).toContain(':NickServ!NickServ@services NOTICE bob :Nick: bob');
|
|
560
|
+
expect(texts).toContain(':NickServ!NickServ@services NOTICE bob :Email: bob@example.com');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('shows Email to an oper even when not the owner', () => {
|
|
382
564
|
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
383
565
|
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
384
566
|
const conn = makeConn();
|
|
567
|
+
conn.userModes.oper = true;
|
|
385
568
|
const ctx = makeCtx(conn, services);
|
|
386
569
|
|
|
387
570
|
const out = nickservReducer(conn, privmsg('INFO bob'), ctx);
|
|
@@ -439,7 +622,7 @@ describe('nickservReducer — misc', () => {
|
|
|
439
622
|
const out = nickservReducer(conn, privmsg('FROBNICATE foo bar'), ctx);
|
|
440
623
|
|
|
441
624
|
expect(sentTexts(out.effects)).toContain(
|
|
442
|
-
':NickServ!NickServ@services NOTICE alice :Unknown command. Available: REGISTER, IDENTIFY, DROP, INFO, SET',
|
|
625
|
+
':NickServ!NickServ@services NOTICE alice :Unknown command. Available: REGISTER, IDENTIFY, DROP, INFO, SET (ENFORCE, PASSWORD)',
|
|
443
626
|
);
|
|
444
627
|
});
|
|
445
628
|
|
|
@@ -674,6 +857,242 @@ describe('nickservReducer — SET ENFORCE', () => {
|
|
|
674
857
|
});
|
|
675
858
|
});
|
|
676
859
|
|
|
860
|
+
// ============================================================================
|
|
861
|
+
// SET PASSWORD
|
|
862
|
+
// ============================================================================
|
|
863
|
+
|
|
864
|
+
describe('nickservReducer — SET PASSWORD', () => {
|
|
865
|
+
it('changes the password when the caller supplies the correct old one', () => {
|
|
866
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
867
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
868
|
+
const conn = makeConn();
|
|
869
|
+
conn.account = 'alice';
|
|
870
|
+
conn.userModes.registered = true;
|
|
871
|
+
const ctx = makeCtx(conn, services);
|
|
872
|
+
|
|
873
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
874
|
+
|
|
875
|
+
expect(sentTexts(out.effects)).toContain(
|
|
876
|
+
':NickServ!NickServ@services NOTICE alice :Password changed.',
|
|
877
|
+
);
|
|
878
|
+
// The new password verifies, the old one no longer does.
|
|
879
|
+
expect(services.verifyNick('alice', 'newpassword').ok).toBe(true);
|
|
880
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(false);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('rejects a wrong old password and leaves the credential unchanged', () => {
|
|
884
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
885
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
886
|
+
const conn = makeConn();
|
|
887
|
+
conn.account = 'alice';
|
|
888
|
+
conn.userModes.registered = true;
|
|
889
|
+
const ctx = makeCtx(conn, services);
|
|
890
|
+
|
|
891
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD wrongold newpassword'), ctx);
|
|
892
|
+
|
|
893
|
+
expect(sentTexts(out.effects)).toContain(
|
|
894
|
+
':NickServ!NickServ@services NOTICE alice :Invalid password.',
|
|
895
|
+
);
|
|
896
|
+
// The original password still verifies; the new one does not.
|
|
897
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
898
|
+
expect(services.verifyNick('alice', 'newpassword').ok).toBe(false);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('requires identification before SET PASSWORD (unidentified caller)', () => {
|
|
902
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
903
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
904
|
+
const conn = makeConn();
|
|
905
|
+
// No account / +r.
|
|
906
|
+
const ctx = makeCtx(conn, services);
|
|
907
|
+
|
|
908
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
909
|
+
|
|
910
|
+
expect(sentTexts(out.effects)).toContain(
|
|
911
|
+
':NickServ!NickServ@services NOTICE alice :You must identify before changing your password.',
|
|
912
|
+
);
|
|
913
|
+
// Credential unchanged.
|
|
914
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
915
|
+
expect(services.verifyNick('alice', 'newpassword').ok).toBe(false);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('rejects SET PASSWORD when identified as a different account', () => {
|
|
919
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
920
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
921
|
+
// bob is identified, but is using alice's nick.
|
|
922
|
+
services.registerNick('bob', 'bobpw', 'bob@example.com');
|
|
923
|
+
const conn = makeConn('c1', 'alice');
|
|
924
|
+
conn.account = 'bob';
|
|
925
|
+
conn.userModes.registered = true;
|
|
926
|
+
const ctx = makeCtx(conn, services);
|
|
927
|
+
|
|
928
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
929
|
+
|
|
930
|
+
expect(sentTexts(out.effects)).toContain(
|
|
931
|
+
':NickServ!NickServ@services NOTICE alice :You must identify before changing your password.',
|
|
932
|
+
);
|
|
933
|
+
// Credential unchanged.
|
|
934
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
935
|
+
expect(services.verifyNick('alice', 'newpassword').ok).toBe(false);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('emits the syntax notice when args are missing', () => {
|
|
939
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
940
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
941
|
+
const conn = makeConn();
|
|
942
|
+
conn.account = 'alice';
|
|
943
|
+
conn.userModes.registered = true;
|
|
944
|
+
const ctx = makeCtx(conn, services);
|
|
945
|
+
|
|
946
|
+
const noArgs = nickservReducer(conn, privmsg('SET PASSWORD'), ctx);
|
|
947
|
+
expect(sentTexts(noArgs.effects)).toContain(
|
|
948
|
+
':NickServ!NickServ@services NOTICE alice :Syntax: SET PASSWORD <old-password> <new-password>',
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
const oneArg = nickservReducer(conn, privmsg('SET PASSWORD only-one'), ctx);
|
|
952
|
+
expect(sentTexts(oneArg.effects)).toContain(
|
|
953
|
+
':NickServ!NickServ@services NOTICE alice :Syntax: SET PASSWORD <old-password> <new-password>',
|
|
954
|
+
);
|
|
955
|
+
// Credential unchanged.
|
|
956
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('rejects a new password shorter than the default minimum (8)', () => {
|
|
960
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
961
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
962
|
+
const conn = makeConn();
|
|
963
|
+
conn.account = 'alice';
|
|
964
|
+
conn.userModes.registered = true;
|
|
965
|
+
const ctx = makeCtx(conn, services);
|
|
966
|
+
|
|
967
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 short'), ctx);
|
|
968
|
+
|
|
969
|
+
expect(sentTexts(out.effects)).toContain(
|
|
970
|
+
':NickServ!NickServ@services NOTICE alice :Password is too short (minimum 8 characters).',
|
|
971
|
+
);
|
|
972
|
+
// Credential unchanged.
|
|
973
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
974
|
+
expect(services.verifyNick('alice', 'short').ok).toBe(false);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('honours a configured nickServ.minPasswordLength override', () => {
|
|
978
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
979
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
980
|
+
const conn = makeConn();
|
|
981
|
+
conn.account = 'alice';
|
|
982
|
+
conn.userModes.registered = true;
|
|
983
|
+
const ctx: Ctx = buildCtx({
|
|
984
|
+
serverConfig: { ...serverConfig, nickServ: { minPasswordLength: 12 } },
|
|
985
|
+
clock: new FakeClock(NOW),
|
|
986
|
+
ids: new SequentialIdFactory(),
|
|
987
|
+
motd: EmptyMotdProvider,
|
|
988
|
+
connection: conn,
|
|
989
|
+
services,
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 elevenchars'), ctx);
|
|
993
|
+
|
|
994
|
+
expect(sentTexts(out.effects)).toContain(
|
|
995
|
+
':NickServ!NickServ@services NOTICE alice :Password is too short (minimum 12 characters).',
|
|
996
|
+
);
|
|
997
|
+
// Credential unchanged.
|
|
998
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('rejects a new password longer than the maximum (256)', () => {
|
|
1002
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1003
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1004
|
+
const conn = makeConn();
|
|
1005
|
+
conn.account = 'alice';
|
|
1006
|
+
conn.userModes.registered = true;
|
|
1007
|
+
const ctx = makeCtx(conn, services);
|
|
1008
|
+
|
|
1009
|
+
const tooLong = 'x'.repeat(257);
|
|
1010
|
+
const out = nickservReducer(conn, privmsg(`SET PASSWORD hunter2 ${tooLong}`), ctx);
|
|
1011
|
+
|
|
1012
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1013
|
+
':NickServ!NickServ@services NOTICE alice :Password is too long (maximum 256 characters).',
|
|
1014
|
+
);
|
|
1015
|
+
// Credential unchanged.
|
|
1016
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it('keeps the session +r after a successful change (no re-identify required)', () => {
|
|
1020
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1021
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1022
|
+
const conn = makeConn();
|
|
1023
|
+
conn.account = 'alice';
|
|
1024
|
+
conn.userModes.registered = true;
|
|
1025
|
+
const ctx = makeCtx(conn, services);
|
|
1026
|
+
|
|
1027
|
+
nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
1028
|
+
|
|
1029
|
+
expect(conn.userModes.registered).toBe(true);
|
|
1030
|
+
expect(conn.account).toBe('alice');
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
it('accepts a new password exactly at the minimum length boundary (8)', () => {
|
|
1034
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1035
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1036
|
+
const conn = makeConn();
|
|
1037
|
+
conn.account = 'alice';
|
|
1038
|
+
conn.userModes.registered = true;
|
|
1039
|
+
const ctx = makeCtx(conn, services);
|
|
1040
|
+
|
|
1041
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 exactly8'), ctx);
|
|
1042
|
+
|
|
1043
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1044
|
+
':NickServ!NickServ@services NOTICE alice :Password changed.',
|
|
1045
|
+
);
|
|
1046
|
+
expect(services.verifyNick('alice', 'exactly8').ok).toBe(true);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it('accepts a new password exactly at the maximum length boundary (256)', () => {
|
|
1050
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1051
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1052
|
+
const conn = makeConn();
|
|
1053
|
+
conn.account = 'alice';
|
|
1054
|
+
conn.userModes.registered = true;
|
|
1055
|
+
const ctx = makeCtx(conn, services);
|
|
1056
|
+
|
|
1057
|
+
const boundary = 'x'.repeat(256);
|
|
1058
|
+
const out = nickservReducer(conn, privmsg(`SET PASSWORD hunter2 ${boundary}`), ctx);
|
|
1059
|
+
|
|
1060
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1061
|
+
':NickServ!NickServ@services NOTICE alice :Password changed.',
|
|
1062
|
+
);
|
|
1063
|
+
expect(services.verifyNick('alice', boundary).ok).toBe(true);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('is case-insensitive on the PASSWORD subkey', () => {
|
|
1067
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1068
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1069
|
+
const conn = makeConn();
|
|
1070
|
+
conn.account = 'alice';
|
|
1071
|
+
conn.userModes.registered = true;
|
|
1072
|
+
const ctx = makeCtx(conn, services);
|
|
1073
|
+
|
|
1074
|
+
const out = nickservReducer(conn, privmsg('set password hunter2 newpassword'), ctx);
|
|
1075
|
+
|
|
1076
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1077
|
+
':NickServ!NickServ@services NOTICE alice :Password changed.',
|
|
1078
|
+
);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('rejects SET PASSWORD on an unregistered nick', () => {
|
|
1082
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1083
|
+
const conn = makeConn();
|
|
1084
|
+
conn.account = 'alice';
|
|
1085
|
+
conn.userModes.registered = true;
|
|
1086
|
+
const ctx = makeCtx(conn, services);
|
|
1087
|
+
|
|
1088
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
1089
|
+
|
|
1090
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1091
|
+
':NickServ!NickServ@services NOTICE alice :Nick alice is not registered.',
|
|
1092
|
+
);
|
|
1093
|
+
});
|
|
1094
|
+
});
|
|
1095
|
+
|
|
677
1096
|
// ============================================================================
|
|
678
1097
|
// enforceRegisteredNick — the enforcement hook fired by the NICK path
|
|
679
1098
|
// ============================================================================
|
|
@@ -81,6 +81,21 @@ describe('operReducer — successful authentication', () => {
|
|
|
81
81
|
expect(modeLines).toContain(':alice!alice@example.com MODE alice +o');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it('falls back to ? / * in the MODE echo when the connection has no nick or hostmask', () => {
|
|
85
|
+
// A bare connection with no nick/user/host: hostmaskOf returns undefined
|
|
86
|
+
// and state.nick is undefined, so modeEchoLine falls back to '?' / '*'.
|
|
87
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
88
|
+
const ctx = makeCtx(state);
|
|
89
|
+
|
|
90
|
+
const out = operReducer(state, oper('alice', 'secret'), ctx);
|
|
91
|
+
|
|
92
|
+
expect(out.state.userModes.oper).toBe(true);
|
|
93
|
+
const modeLines = out.effects
|
|
94
|
+
.filter((e): e is Extract<EffectType, { tag: 'Send' }> => e.tag === 'Send')
|
|
95
|
+
.flatMap((e) => e.lines.map((l) => l.text));
|
|
96
|
+
expect(modeLines).toContain(':? MODE * +o');
|
|
97
|
+
});
|
|
98
|
+
|
|
84
99
|
it('matches the second configured credential pair', () => {
|
|
85
100
|
const state = makeState();
|
|
86
101
|
const ctx = makeCtx(state);
|