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,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import pkg from '../../package.json' with { type: 'json' };
|
|
3
3
|
import { cloakHost } from '../../src/cloak';
|
|
4
4
|
import {
|
|
@@ -27,14 +27,7 @@ import {
|
|
|
27
27
|
SequentialIdFactory,
|
|
28
28
|
StaticMotdProvider,
|
|
29
29
|
} from '../../src/ports';
|
|
30
|
-
import type {
|
|
31
|
-
AccountStore,
|
|
32
|
-
AwayStore,
|
|
33
|
-
MotdProvider,
|
|
34
|
-
SaslPayload,
|
|
35
|
-
SaslResult,
|
|
36
|
-
ServicesStore,
|
|
37
|
-
} from '../../src/ports';
|
|
30
|
+
import type { AwayStore, MotdProvider, ServicesStore } from '../../src/ports';
|
|
38
31
|
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
39
32
|
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
40
33
|
|
|
@@ -615,8 +608,9 @@ describe('passReducer', () => {
|
|
|
615
608
|
describe('passReducer — post-registration PASS <nick>:<password>', () => {
|
|
616
609
|
it('logs in when PASS <nick>:<password> matches an account after registration', () => {
|
|
617
610
|
const state = registeredState();
|
|
618
|
-
const
|
|
619
|
-
|
|
611
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
612
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
613
|
+
const ctx = makeCtxWithServices(state, services);
|
|
620
614
|
const out = passReducer(state, { command: 'PASS', params: ['alice:hunter2'], tags: {} }, ctx);
|
|
621
615
|
|
|
622
616
|
expect(out.state.account).toBe('alice');
|
|
@@ -632,8 +626,9 @@ describe('passReducer — post-registration PASS <nick>:<password>', () => {
|
|
|
632
626
|
|
|
633
627
|
it('does not log in or disconnect when the password is wrong', () => {
|
|
634
628
|
const state = registeredState();
|
|
635
|
-
const
|
|
636
|
-
|
|
629
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
630
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
631
|
+
const ctx = makeCtxWithServices(state, services);
|
|
637
632
|
const out = passReducer(state, { command: 'PASS', params: ['alice:wrong'], tags: {} }, ctx);
|
|
638
633
|
|
|
639
634
|
expect(out.state.account).toBeUndefined();
|
|
@@ -645,8 +640,8 @@ describe('passReducer — post-registration PASS <nick>:<password>', () => {
|
|
|
645
640
|
it('is indistinguishable from an unknown nick (no auth numerics, no disconnect)', () => {
|
|
646
641
|
const state = registeredState();
|
|
647
642
|
state.nick = 'nobody';
|
|
648
|
-
const
|
|
649
|
-
const ctx =
|
|
643
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
644
|
+
const ctx = makeCtxWithServices(state, services);
|
|
650
645
|
const out = passReducer(state, { command: 'PASS', params: ['nobody:wrong'], tags: {} }, ctx);
|
|
651
646
|
|
|
652
647
|
expect(out.state.account).toBeUndefined();
|
|
@@ -655,55 +650,63 @@ describe('passReducer — post-registration PASS <nick>:<password>', () => {
|
|
|
655
650
|
|
|
656
651
|
it('equalises timing with an extra dummy verify on failure', () => {
|
|
657
652
|
const state = registeredState();
|
|
658
|
-
const
|
|
659
|
-
|
|
653
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
654
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
655
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
656
|
+
const ctx = makeCtxWithServices(state, services);
|
|
660
657
|
passReducer(state, { command: 'PASS', params: ['alice:wrong'], tags: {} }, ctx);
|
|
661
658
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
expect(
|
|
659
|
+
// Two calls: the real (failed) verify against `alice`, then the
|
|
660
|
+
// timing-equalising dummy verify.
|
|
661
|
+
expect(verifyNick).toHaveBeenCalledTimes(2);
|
|
662
|
+
expect(verifyNick.mock.calls[0]?.[0]).toBe('alice');
|
|
663
|
+
expect(verifyNick.mock.calls[0]?.[1]).toBe('wrong');
|
|
664
|
+
// The second call is the dummy; its nick is the sentinel, not 'alice'.
|
|
665
|
+
expect(verifyNick.mock.calls[1]?.[0]).not.toBe('alice');
|
|
669
666
|
});
|
|
670
667
|
|
|
671
668
|
it('does not log in when the payload nick does not match state.nick', () => {
|
|
672
669
|
const state = registeredState();
|
|
673
|
-
const
|
|
674
|
-
|
|
670
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
671
|
+
services.registerNick('bob', 'hunter2', 'bob@example.com');
|
|
672
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
673
|
+
const ctx = makeCtxWithServices(state, services);
|
|
675
674
|
const out = passReducer(state, { command: 'PASS', params: ['bob:hunter2'], tags: {} }, ctx);
|
|
676
675
|
|
|
677
676
|
expect(out.state.account).toBeUndefined();
|
|
678
|
-
expect(
|
|
677
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
679
678
|
expect(out.effects).toEqual([]);
|
|
680
679
|
});
|
|
681
680
|
|
|
682
681
|
it('ignores a bare PASS value (no colon) after registration — no 462, no disconnect', () => {
|
|
683
682
|
const state = registeredState();
|
|
684
|
-
const
|
|
685
|
-
|
|
683
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
684
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
685
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
686
|
+
const ctx = makeCtxWithServices(state, services);
|
|
686
687
|
const out = passReducer(state, { command: 'PASS', params: ['serverpw'], tags: {} }, ctx);
|
|
687
688
|
|
|
688
689
|
expect(out.state.account).toBeUndefined();
|
|
689
690
|
expect(out.effects).toEqual([]);
|
|
690
|
-
expect(
|
|
691
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
691
692
|
expect(out.effects.some((e) => e.tag === 'Disconnect')).toBe(false);
|
|
692
693
|
});
|
|
693
694
|
|
|
694
695
|
it('ignores a late PASS when the connection is already identified', () => {
|
|
695
696
|
const state = registeredState();
|
|
696
697
|
state.account = 'alice';
|
|
697
|
-
const
|
|
698
|
-
|
|
698
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
699
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
700
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
701
|
+
const ctx = makeCtxWithServices(state, services);
|
|
699
702
|
const out = passReducer(state, { command: 'PASS', params: ['alice:hunter2'], tags: {} }, ctx);
|
|
700
703
|
|
|
701
|
-
expect(
|
|
704
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
702
705
|
expect(out.effects).toEqual([]);
|
|
703
706
|
expect(out.effects.some((e) => e.tag === 'Disconnect')).toBe(false);
|
|
704
707
|
});
|
|
705
708
|
|
|
706
|
-
it('ignores a late PASS <nick>:<password> when no
|
|
709
|
+
it('ignores a late PASS <nick>:<password> when no ServicesStore is bound', () => {
|
|
707
710
|
const state = registeredState();
|
|
708
711
|
const ctx = makeCtx(state);
|
|
709
712
|
const out = passReducer(state, { command: 'PASS', params: ['alice:hunter2'], tags: {} }, ctx);
|
|
@@ -1630,32 +1633,6 @@ describe('emitWelcomeIfReady — OperServ AKILL admission', () => {
|
|
|
1630
1633
|
// PASS-based account auth (NICK + PASS <nick>:<password> + USER → identified)
|
|
1631
1634
|
// ============================================================================
|
|
1632
1635
|
|
|
1633
|
-
/** A capture-and-respond fake AccountStore (mirrors the sasl.test.ts fake). */
|
|
1634
|
-
class FakeAccountStore implements AccountStore {
|
|
1635
|
-
readonly calls: Array<{ mech: string; payload: SaslPayload }> = [];
|
|
1636
|
-
constructor(private readonly result: SaslResult) {}
|
|
1637
|
-
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
1638
|
-
this.calls.push({ mech, payload });
|
|
1639
|
-
return this.result;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
/** Like {@link makeCtx} but also threads a bound AccountStore. */
|
|
1644
|
-
function makeCtxWithAccounts(
|
|
1645
|
-
state: ConnectionState,
|
|
1646
|
-
accounts: AccountStore,
|
|
1647
|
-
clock = new FakeClock(1_000),
|
|
1648
|
-
): Ctx {
|
|
1649
|
-
return buildCtx({
|
|
1650
|
-
serverConfig,
|
|
1651
|
-
clock,
|
|
1652
|
-
ids: new SequentialIdFactory(),
|
|
1653
|
-
motd: EmptyMotdProvider,
|
|
1654
|
-
connection: state,
|
|
1655
|
-
accounts,
|
|
1656
|
-
});
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
1636
|
const SV = 'irc.example.com';
|
|
1660
1637
|
const L2 = (text: string): RawLine => ({ text });
|
|
1661
1638
|
|
|
@@ -1666,8 +1643,9 @@ describe('emitWelcomeIfReady — PASS-based account auth (success)', () => {
|
|
|
1666
1643
|
state.user = 'alice';
|
|
1667
1644
|
state.host = 'example.com';
|
|
1668
1645
|
state.passAttempt = 'alice:hunter2';
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1646
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1647
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1648
|
+
const ctx = makeCtxWithServices(state, services);
|
|
1671
1649
|
|
|
1672
1650
|
const effects = emitWelcomeIfReady(state, ctx);
|
|
1673
1651
|
|
|
@@ -1684,11 +1662,10 @@ describe('emitWelcomeIfReady — PASS-based account auth (success)', () => {
|
|
|
1684
1662
|
|
|
1685
1663
|
// ---- helpers for the broader PASS-auth coverage ----
|
|
1686
1664
|
|
|
1687
|
-
/** Flexible ctx that binds any combination of
|
|
1665
|
+
/** Flexible ctx that binds any combination of services/away stores. */
|
|
1688
1666
|
function makeCtxPassAuth(
|
|
1689
1667
|
state: ConnectionState,
|
|
1690
1668
|
opts: {
|
|
1691
|
-
accounts?: AccountStore;
|
|
1692
1669
|
services?: ServicesStore;
|
|
1693
1670
|
away?: AwayStore;
|
|
1694
1671
|
serverPassword?: string;
|
|
@@ -1703,7 +1680,6 @@ function makeCtxPassAuth(
|
|
|
1703
1680
|
ids: new SequentialIdFactory(),
|
|
1704
1681
|
motd: EmptyMotdProvider,
|
|
1705
1682
|
connection: state,
|
|
1706
|
-
...(opts.accounts !== undefined ? { accounts: opts.accounts } : {}),
|
|
1707
1683
|
...(opts.services !== undefined ? { services: opts.services } : {}),
|
|
1708
1684
|
...(opts.away !== undefined ? { away: opts.away } : {}),
|
|
1709
1685
|
});
|
|
@@ -1715,16 +1691,16 @@ function sentTexts(effects: EffectType[]): string[] {
|
|
|
1715
1691
|
}
|
|
1716
1692
|
|
|
1717
1693
|
describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity with SASL)', () => {
|
|
1718
|
-
it('forwards the parsed credentials to
|
|
1694
|
+
it('forwards the parsed credentials to services.verifyNick as a PLAIN verify', () => {
|
|
1719
1695
|
const state = makeState();
|
|
1720
1696
|
state.nick = 'alice';
|
|
1721
1697
|
state.user = 'alice';
|
|
1722
1698
|
state.passAttempt = 'alice:hunter2';
|
|
1723
|
-
const
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1699
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1700
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1701
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1702
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1703
|
+
expect(verifyNick).toHaveBeenCalledWith('alice', 'hunter2');
|
|
1728
1704
|
});
|
|
1729
1705
|
|
|
1730
1706
|
it('stamps read-only user mode `r` on a successful PASS-auth', () => {
|
|
@@ -1732,8 +1708,9 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1732
1708
|
state.nick = 'alice';
|
|
1733
1709
|
state.user = 'alice';
|
|
1734
1710
|
state.passAttempt = 'alice:hunter2';
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1711
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1712
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1713
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1737
1714
|
expect(state.userModes.registered).toBe(true);
|
|
1738
1715
|
});
|
|
1739
1716
|
|
|
@@ -1742,10 +1719,10 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1742
1719
|
state.nick = 'alice';
|
|
1743
1720
|
state.user = 'alice';
|
|
1744
1721
|
state.passAttempt = 'alice:hunter2';
|
|
1745
|
-
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
1746
1722
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1723
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1747
1724
|
services.setLastReadMarker('alice', '#foo', 'm1');
|
|
1748
|
-
emitWelcomeIfReady(state, makeCtxPassAuth(state, {
|
|
1725
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1749
1726
|
expect(state.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
1750
1727
|
});
|
|
1751
1728
|
|
|
@@ -1754,10 +1731,11 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1754
1731
|
state.nick = 'alice';
|
|
1755
1732
|
state.user = 'alice';
|
|
1756
1733
|
state.passAttempt = 'alice:hunter2';
|
|
1757
|
-
const
|
|
1734
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1735
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1758
1736
|
const away = new InMemoryAwayStore();
|
|
1759
1737
|
away.set('alice', 'brb');
|
|
1760
|
-
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, {
|
|
1738
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services, away }));
|
|
1761
1739
|
expect(state.away).toBe('brb');
|
|
1762
1740
|
expect(sentTexts(effects)).toContain(`:${SV} 306 alice :You have been marked as being away`);
|
|
1763
1741
|
});
|
|
@@ -1771,8 +1749,7 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1771
1749
|
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1772
1750
|
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1773
1751
|
services.recordMemo({ from: 'bob', to: 'alice', body: 'hi', sentAt: 0 });
|
|
1774
|
-
const
|
|
1775
|
-
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { accounts, services }));
|
|
1752
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1776
1753
|
expect(sentTexts(effects).some((t) => t.includes('[Memo #') && t.includes('hi'))).toBe(true);
|
|
1777
1754
|
});
|
|
1778
1755
|
|
|
@@ -1782,10 +1759,10 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1782
1759
|
state.user = 'alice';
|
|
1783
1760
|
state.host = 'real.example.net';
|
|
1784
1761
|
state.passAttempt = 'alice:hunter2';
|
|
1785
|
-
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
1786
1762
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1763
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1787
1764
|
services.setVhost('alice', 'cool.example.net');
|
|
1788
|
-
const ctx = makeCtxPassAuth(state, {
|
|
1765
|
+
const ctx = makeCtxPassAuth(state, { services });
|
|
1789
1766
|
|
|
1790
1767
|
const effects = emitWelcomeIfReady(state, ctx);
|
|
1791
1768
|
|
|
@@ -1804,9 +1781,9 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1804
1781
|
state.user = 'alice';
|
|
1805
1782
|
state.host = 'real.example.net';
|
|
1806
1783
|
state.passAttempt = 'alice:hunter2';
|
|
1807
|
-
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
1808
1784
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1809
|
-
|
|
1785
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1786
|
+
const ctx = makeCtxPassAuth(state, { services });
|
|
1810
1787
|
|
|
1811
1788
|
emitWelcomeIfReady(state, ctx);
|
|
1812
1789
|
|
|
@@ -1820,8 +1797,8 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1820
1797
|
state.user = 'alice';
|
|
1821
1798
|
state.host = '203.0.113.5';
|
|
1822
1799
|
state.passAttempt = 'alice:hunter2';
|
|
1823
|
-
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
1824
1800
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1801
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1825
1802
|
services.setVhost('alice', 'cool.example.net');
|
|
1826
1803
|
const ctx: Ctx = buildCtx({
|
|
1827
1804
|
serverConfig: { ...serverConfig, cloaking: { enabled: true, secret: 's3cret' } },
|
|
@@ -1829,7 +1806,6 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1829
1806
|
ids: new SequentialIdFactory(),
|
|
1830
1807
|
motd: EmptyMotdProvider,
|
|
1831
1808
|
connection: state,
|
|
1832
|
-
accounts,
|
|
1833
1809
|
services,
|
|
1834
1810
|
});
|
|
1835
1811
|
|
|
@@ -1851,8 +1827,9 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1851
1827
|
state.nick = 'alice';
|
|
1852
1828
|
state.user = 'alice';
|
|
1853
1829
|
state.passAttempt = 'alice:hunter2';
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1830
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1831
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1832
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1856
1833
|
expect(state.account).toBe('alice');
|
|
1857
1834
|
});
|
|
1858
1835
|
});
|
|
@@ -1863,8 +1840,9 @@ describe('emitWelcomeIfReady — PASS-based account auth (failures)', () => {
|
|
|
1863
1840
|
state.nick = 'alice';
|
|
1864
1841
|
state.user = 'alice';
|
|
1865
1842
|
state.passAttempt = 'alice:wrong';
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1843
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1844
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1845
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1868
1846
|
|
|
1869
1847
|
expect(state.account).toBeUndefined();
|
|
1870
1848
|
expect(state.userModes.registered).toBe(false);
|
|
@@ -1880,8 +1858,8 @@ describe('emitWelcomeIfReady — PASS-based account auth (failures)', () => {
|
|
|
1880
1858
|
state.nick = 'nobody';
|
|
1881
1859
|
state.user = 'nobody';
|
|
1882
1860
|
state.passAttempt = 'nobody:wrong';
|
|
1883
|
-
const
|
|
1884
|
-
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, {
|
|
1861
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1862
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1885
1863
|
|
|
1886
1864
|
expect(state.account).toBeUndefined();
|
|
1887
1865
|
expect(state.registration).toBe('registered');
|
|
@@ -1897,17 +1875,15 @@ describe('emitWelcomeIfReady — PASS-based account auth (failures)', () => {
|
|
|
1897
1875
|
state.nick = 'alice';
|
|
1898
1876
|
state.user = 'alice';
|
|
1899
1877
|
state.passAttempt = 'alice:wrong';
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
expect(accounts.calls[1]?.mech).toBe('PLAIN');
|
|
1910
|
-
expect(accounts.calls[1]?.payload.kind).toBe('PLAIN');
|
|
1878
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1879
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1880
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1881
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1882
|
+
|
|
1883
|
+
expect(verifyNick).toHaveBeenCalledTimes(2);
|
|
1884
|
+
expect(verifyNick.mock.calls[0]?.[0]).toBe('alice');
|
|
1885
|
+
expect(verifyNick.mock.calls[0]?.[1]).toBe('wrong');
|
|
1886
|
+
expect(verifyNick.mock.calls[1]?.[0]).not.toBe('alice');
|
|
1911
1887
|
});
|
|
1912
1888
|
|
|
1913
1889
|
it('treats a payload nick that does not match state.nick as a failed auth (no silent login)', () => {
|
|
@@ -1915,19 +1891,21 @@ describe('emitWelcomeIfReady — PASS-based account auth (failures)', () => {
|
|
|
1915
1891
|
state.nick = 'alice';
|
|
1916
1892
|
state.user = 'alice';
|
|
1917
1893
|
state.passAttempt = 'bob:hunter2';
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1894
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1895
|
+
services.registerNick('bob', 'hunter2', 'bob@example.com');
|
|
1896
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1897
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1920
1898
|
|
|
1921
1899
|
expect(state.account).toBeUndefined();
|
|
1922
1900
|
expect(state.registration).toBe('registered');
|
|
1923
1901
|
// The store is never consulted on a nick mismatch.
|
|
1924
|
-
expect(
|
|
1902
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1925
1903
|
expect(sentTexts(effects).some((t) => t.includes(' 900 '))).toBe(false);
|
|
1926
1904
|
});
|
|
1927
1905
|
});
|
|
1928
1906
|
|
|
1929
1907
|
describe('emitWelcomeIfReady — PASS-auth interaction with the server-password gate', () => {
|
|
1930
|
-
it('ignores the <nick>:<password> form when no
|
|
1908
|
+
it('ignores the <nick>:<password> form when no ServicesStore is bound (open gate)', () => {
|
|
1931
1909
|
const state = makeState();
|
|
1932
1910
|
state.nick = 'alice';
|
|
1933
1911
|
state.user = 'alice';
|
|
@@ -1939,7 +1917,28 @@ describe('emitWelcomeIfReady — PASS-auth interaction with the server-password
|
|
|
1939
1917
|
expect(sentTexts(effects).some((t) => t.includes(' 464 '))).toBe(false);
|
|
1940
1918
|
});
|
|
1941
1919
|
|
|
1942
|
-
it('
|
|
1920
|
+
it('a SASL-identified connection (state.account set) satisfies the gate with no credential-store read', () => {
|
|
1921
|
+
const state = makeState();
|
|
1922
|
+
state.nick = 'alice';
|
|
1923
|
+
state.user = 'alice';
|
|
1924
|
+
state.account = 'alice'; // SASL-identified before registration completion.
|
|
1925
|
+
state.passAttempt = 'alice:hunter2';
|
|
1926
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1927
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1928
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1929
|
+
const effects = emitWelcomeIfReady(
|
|
1930
|
+
state,
|
|
1931
|
+
makeCtxPassAuth(state, { services, serverPassword: 's3cret' }),
|
|
1932
|
+
);
|
|
1933
|
+
|
|
1934
|
+
// The gate is satisfied by state.account; no PASS-auth verify fires.
|
|
1935
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1936
|
+
expect(state.registration).toBe('registered');
|
|
1937
|
+
expect(sentTexts(effects).some((t) => t.includes(' 464 '))).toBe(false);
|
|
1938
|
+
expect(sentTexts(effects).some((t) => t.includes(' 900 '))).toBe(false);
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
it('collapses to the server-password gate (464) when no ServicesStore is bound and the colon form does not match', () => {
|
|
1943
1942
|
const state = makeState();
|
|
1944
1943
|
state.nick = 'alice';
|
|
1945
1944
|
state.user = 'alice';
|
|
@@ -1958,10 +1957,11 @@ describe('emitWelcomeIfReady — PASS-auth interaction with the server-password
|
|
|
1958
1957
|
state.nick = 'alice';
|
|
1959
1958
|
state.user = 'alice';
|
|
1960
1959
|
state.passAttempt = 'alice:wrong';
|
|
1961
|
-
const
|
|
1960
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1961
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1962
1962
|
const effects = emitWelcomeIfReady(
|
|
1963
1963
|
state,
|
|
1964
|
-
makeCtxPassAuth(state, {
|
|
1964
|
+
makeCtxPassAuth(state, { services, serverPassword: 's3cret' }),
|
|
1965
1965
|
);
|
|
1966
1966
|
|
|
1967
1967
|
expect(state.registration).not.toBe('registered');
|
|
@@ -1978,10 +1978,12 @@ describe('emitWelcomeIfReady — PASS-auth interaction with the server-password
|
|
|
1978
1978
|
state.user = 'alice';
|
|
1979
1979
|
state.account = 'alice'; // SASL-identified before registration completion
|
|
1980
1980
|
state.passAttempt = 'alice:hunter2';
|
|
1981
|
-
const
|
|
1982
|
-
|
|
1981
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1982
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1983
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1984
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1983
1985
|
|
|
1984
|
-
expect(
|
|
1986
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1985
1987
|
expect(state.registration).toBe('registered');
|
|
1986
1988
|
// No 900/903 are re-emitted by PASS-auth; SASL already emitted them.
|
|
1987
1989
|
expect(sentTexts(effects).some((t) => t.includes(' 900 '))).toBe(false);
|
|
@@ -1992,13 +1994,15 @@ describe('emitWelcomeIfReady — PASS-auth interaction with the server-password
|
|
|
1992
1994
|
state.nick = 'alice';
|
|
1993
1995
|
state.user = 'alice';
|
|
1994
1996
|
state.passAttempt = 's3cret';
|
|
1995
|
-
const
|
|
1997
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1998
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1999
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1996
2000
|
const effects = emitWelcomeIfReady(
|
|
1997
2001
|
state,
|
|
1998
|
-
makeCtxPassAuth(state, {
|
|
2002
|
+
makeCtxPassAuth(state, { services, serverPassword: 's3cret' }),
|
|
1999
2003
|
);
|
|
2000
2004
|
|
|
2001
|
-
expect(
|
|
2005
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
2002
2006
|
expect(state.account).toBeUndefined();
|
|
2003
2007
|
expect(state.registration).toBe('registered');
|
|
2004
2008
|
expect(sentTexts(effects).some((t) => t.includes(' 464 '))).toBe(false);
|
|
@@ -2011,8 +2015,9 @@ describe('passBasedAccountAuth — unit (PASS before NICK, parsing, edge cases)'
|
|
|
2011
2015
|
state.user = 'alice';
|
|
2012
2016
|
// state.nick intentionally undefined — PASS arrived before NICK.
|
|
2013
2017
|
state.passAttempt = 'alice:hunter2';
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2018
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2019
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2020
|
+
const effects = passBasedAccountAuth(state, makeCtxPassAuth(state, { services }));
|
|
2016
2021
|
|
|
2017
2022
|
expect(state.account).toBe('alice');
|
|
2018
2023
|
expect(effects.some((e) => e.tag === 'Send')).toBe(true);
|
|
@@ -2021,26 +2026,31 @@ describe('passBasedAccountAuth — unit (PASS before NICK, parsing, edge cases)'
|
|
|
2021
2026
|
it('returns no effects when no PASS was sent', () => {
|
|
2022
2027
|
const state = makeState();
|
|
2023
2028
|
state.nick = 'alice';
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2029
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2030
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2031
|
+
expect(passBasedAccountAuth(state, makeCtxPassAuth(state, { services }))).toEqual([]);
|
|
2026
2032
|
});
|
|
2027
2033
|
|
|
2028
2034
|
it('returns no effects for a bare PASS value (no colon)', () => {
|
|
2029
2035
|
const state = makeState();
|
|
2030
2036
|
state.nick = 'alice';
|
|
2031
2037
|
state.passAttempt = 's3cret';
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
2038
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2039
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2040
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
2041
|
+
expect(passBasedAccountAuth(state, makeCtxPassAuth(state, { services }))).toEqual([]);
|
|
2042
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
2035
2043
|
});
|
|
2036
2044
|
|
|
2037
2045
|
it('returns no effects when the nick portion is empty (`:password`)', () => {
|
|
2038
2046
|
const state = makeState();
|
|
2039
2047
|
state.nick = 'alice';
|
|
2040
2048
|
state.passAttempt = ':hunter2';
|
|
2041
|
-
const
|
|
2042
|
-
|
|
2043
|
-
|
|
2049
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2050
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2051
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
2052
|
+
expect(passBasedAccountAuth(state, makeCtxPassAuth(state, { services }))).toEqual([]);
|
|
2053
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
2044
2054
|
});
|
|
2045
2055
|
|
|
2046
2056
|
it('splits only on the first colon so a password may contain colons', () => {
|
|
@@ -2048,16 +2058,13 @@ describe('passBasedAccountAuth — unit (PASS before NICK, parsing, edge cases)'
|
|
|
2048
2058
|
state.nick = 'alice';
|
|
2049
2059
|
state.user = 'alice';
|
|
2050
2060
|
state.passAttempt = 'alice:p:ass:word';
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
username: 'alice',
|
|
2056
|
-
password: 'p:ass:word',
|
|
2057
|
-
});
|
|
2061
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2062
|
+
services.registerNick('alice', 'p:ass:word', 'alice@example.com');
|
|
2063
|
+
passBasedAccountAuth(state, makeCtxPassAuth(state, { services }));
|
|
2064
|
+
expect(state.account).toBe('alice');
|
|
2058
2065
|
});
|
|
2059
2066
|
|
|
2060
|
-
it('returns no effects when no
|
|
2067
|
+
it('returns no effects when no ServicesStore is bound', () => {
|
|
2061
2068
|
const state = makeState();
|
|
2062
2069
|
state.nick = 'alice';
|
|
2063
2070
|
state.passAttempt = 'alice:hunter2';
|