serverless-ircd 0.9.0 → 0.11.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 +28 -0
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +35 -9
- package/.github/workflows/deploy-cf.yml +40 -14
- package/CHANGELOG.md +594 -0
- package/README.md +286 -60
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +118 -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 +398 -67
- 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 +714 -105
- 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/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -3
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +267 -87
- package/apps/cf-tcp-container/src/main.ts +22 -7
- package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
- package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
- package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
- package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
- package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
- package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
- package/apps/cf-tcp-container/wrangler.toml +18 -14
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/src/worker.ts +77 -5
- package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
- package/apps/cf-worker/tests/smoke.test.ts +4 -0
- package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
- package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
- package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
- package/apps/cf-worker/wrangler.test.toml +15 -1
- package/apps/cf-worker/wrangler.toml +95 -77
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +14 -2
- package/apps/local-cli/src/line-scanner.ts +26 -0
- package/apps/local-cli/src/server.ts +44 -19
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- 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 +793 -118
- package/docs/AWS-TCP-Deployment.md +57 -47
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +95 -114
- package/docs/Cloudflare-TCP-Deployment.md +160 -101
- package/docs/Release-Process.md +27 -23
- package/docs/SASL-EXTERNAL.md +175 -0
- 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/admission.ts +28 -13
- package/packages/aws-adapter/src/aws-runtime.ts +30 -3
- package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
- package/packages/aws-adapter/src/config-loader.ts +153 -8
- package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
- package/packages/aws-adapter/src/handlers/connect.ts +73 -1
- package/packages/aws-adapter/src/handlers/default.ts +279 -123
- package/packages/aws-adapter/src/handlers/index.ts +98 -25
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +23 -0
- package/packages/aws-adapter/src/tables.ts +11 -12
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
- package/packages/aws-adapter/tests/connect.test.ts +323 -3
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
- package/packages/aws-adapter/tests/handlers.test.ts +174 -12
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- 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 +100 -10
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +406 -116
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +105 -26
- package/packages/cf-adapter/src/env.ts +99 -10
- package/packages/cf-adapter/src/index.ts +17 -7
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
- package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
- package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
- package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/cf-adapter/tests/worker/main.ts +4 -0
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/cap.ts +10 -2
- package/packages/irc-core/src/commands/chanserv.ts +117 -14
- package/packages/irc-core/src/commands/chathistory.ts +13 -5
- package/packages/irc-core/src/commands/hostserv.ts +84 -8
- package/packages/irc-core/src/commands/index.ts +2 -1
- package/packages/irc-core/src/commands/invite.ts +1 -7
- package/packages/irc-core/src/commands/join.ts +1 -16
- package/packages/irc-core/src/commands/kick.ts +1 -8
- package/packages/irc-core/src/commands/list.ts +1 -8
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/mode.ts +1 -8
- package/packages/irc-core/src/commands/multiline.ts +4 -10
- package/packages/irc-core/src/commands/names.ts +53 -13
- package/packages/irc-core/src/commands/nickserv.ts +161 -11
- package/packages/irc-core/src/commands/oper.ts +361 -8
- package/packages/irc-core/src/commands/part.ts +4 -10
- package/packages/irc-core/src/commands/privmsg.ts +8 -4
- package/packages/irc-core/src/commands/registration.ts +148 -4
- package/packages/irc-core/src/commands/sasl.ts +154 -46
- package/packages/irc-core/src/commands/topic.ts +10 -12
- package/packages/irc-core/src/commands/who.ts +1 -8
- package/packages/irc-core/src/config.ts +424 -25
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/effects.ts +24 -0
- package/packages/irc-core/src/flood-control.ts +10 -10
- package/packages/irc-core/src/frame-rate-limit.ts +82 -0
- package/packages/irc-core/src/index.ts +9 -0
- package/packages/irc-core/src/oper-hashing.ts +43 -0
- package/packages/irc-core/src/oper-lockout.ts +87 -0
- package/packages/irc-core/src/ports.ts +529 -190
- package/packages/irc-core/src/protocol/bytes.ts +65 -0
- package/packages/irc-core/src/protocol/channel-name.ts +37 -0
- package/packages/irc-core/src/protocol/index.ts +12 -1
- package/packages/irc-core/src/protocol/outbound.ts +43 -10
- package/packages/irc-core/src/protocol/parser.ts +79 -10
- package/packages/irc-core/src/state/connection.ts +13 -0
- package/packages/irc-core/src/types.ts +266 -23
- package/packages/irc-core/src/ws-framing.ts +5 -4
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/bytes.test.ts +89 -0
- package/packages/irc-core/tests/certfp.test.ts +117 -0
- package/packages/irc-core/tests/commands/cap.test.ts +76 -2
- package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
- package/packages/irc-core/tests/commands/join.test.ts +78 -1
- 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/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
- package/packages/irc-core/tests/commands/oper.test.ts +574 -1
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +602 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
- package/packages/irc-core/tests/config.test.ts +534 -2
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/flood-control.test.ts +29 -1
- package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
- package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
- package/packages/irc-core/tests/outbound.test.ts +148 -0
- package/packages/irc-core/tests/parser.test.ts +287 -5
- package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
- package/packages/irc-core/tests/ports.test.ts +170 -7
- package/packages/irc-core/tests/services-store.test.ts +567 -1
- package/packages/irc-core/tests/ws-framing.test.ts +45 -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 +123 -22
- package/packages/irc-server/src/dispatch.ts +1 -0
- package/packages/irc-server/src/index.ts +7 -0
- package/packages/irc-server/src/redact.ts +159 -0
- package/packages/irc-server/src/runtime.ts +14 -0
- package/packages/irc-server/src/transport.ts +28 -1
- package/packages/irc-server/tests/actor.test.ts +563 -54
- package/packages/irc-server/tests/dispatch.test.ts +31 -0
- package/packages/irc-server/tests/redact.test.ts +198 -0
- package/packages/irc-server/tests/runtime.test.ts +2 -0
- package/packages/irc-server/tests/transport.test.ts +66 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
- 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 +2 -2
- package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
- package/tools/ci-hardening/src/cf-deploy.ts +118 -0
- package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
- package/tools/ci-hardening/src/env-var-drift.ts +192 -0
- package/tools/ci-hardening/src/hostname-guard.ts +11 -0
- package/tools/ci-hardening/src/index.ts +19 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
- package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
- package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
- package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/ci-hardening/vitest.config.ts +5 -1
- package/tools/hash-oper-cred.ts +85 -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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
CERT_FP_PREFIX,
|
|
4
|
+
canonicalizeCertSubject,
|
|
5
|
+
certFingerprintId,
|
|
6
|
+
certIdentityFromSubject,
|
|
7
|
+
} from '../src/certfp';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// canonicalizeCertSubject — RFC 4514-style DN canonicalisation
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
describe('canonicalizeCertSubject', () => {
|
|
14
|
+
it('lowercases the attribute type but preserves the value case', () => {
|
|
15
|
+
expect(canonicalizeCertSubject('CN=Alice')).toBe('cn=Alice');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('collapses repeated whitespace inside a value', () => {
|
|
19
|
+
expect(canonicalizeCertSubject('CN=alice smith')).toBe('cn=alice smith');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('trims whitespace around the type and value', () => {
|
|
23
|
+
expect(canonicalizeCertSubject(' CN = alice ')).toBe('cn=alice');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('sorts RDNs into a stable order regardless of input ordering', () => {
|
|
27
|
+
expect(canonicalizeCertSubject('O=Example Inc,CN=alice')).toBe('cn=alice,o=Example Inc');
|
|
28
|
+
expect(canonicalizeCertSubject('CN=alice,O=Example Inc')).toBe('cn=alice,o=Example Inc');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('sorts AVAs inside a multi-valued RDN regardless of input ordering', () => {
|
|
32
|
+
expect(canonicalizeCertSubject('O=Example+CN=alice')).toBe('cn=alice+o=Example');
|
|
33
|
+
expect(canonicalizeCertSubject('CN=alice+O=Example')).toBe('cn=alice+o=Example');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('collapses two semantically-equal DNs (spacing + ordering differ) to one form', () => {
|
|
37
|
+
const a = canonicalizeCertSubject('CN=alice , O = Example Inc , C=US');
|
|
38
|
+
const b = canonicalizeCertSubject('C=US,O=Example Inc,CN=alice');
|
|
39
|
+
expect(a).toBe(b);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not split on an escaped separator', () => {
|
|
43
|
+
expect(canonicalizeCertSubject('CN=a\\,b,O=x')).toBe('cn=a\\,b,o=x');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('skips escaped characters when locating the type/value split', () => {
|
|
47
|
+
expect(canonicalizeCertSubject('CN\\=tag=y')).toBe('cn\\=tag=y');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not trim an escaped trailing space from a value', () => {
|
|
51
|
+
expect(canonicalizeCertSubject('CN=alice\\ ')).toBe('cn=alice\\ ');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps an escape pair intact when a pending space precedes it', () => {
|
|
55
|
+
expect(canonicalizeCertSubject('CN=a \\, b')).toBe('cn=a \\, b');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('keeps a trailing dangling backslash verbatim', () => {
|
|
59
|
+
expect(canonicalizeCertSubject('CN=a\\')).toBe('cn=a\\');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('keeps a malformed AVA without an equals sign as trimmed collapsed text', () => {
|
|
63
|
+
expect(canonicalizeCertSubject(' alice smith ')).toBe('alice smith');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('canonicalises an empty DN to the empty string', () => {
|
|
67
|
+
expect(canonicalizeCertSubject('')).toBe('');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// certFingerprintId — normalised DER SHA-256 fingerprint binding id
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
describe('certFingerprintId', () => {
|
|
76
|
+
it('prefixes the fingerprint with the binding prefix', () => {
|
|
77
|
+
expect(certFingerprintId('af363dc8')).toBe(`${CERT_FP_PREFIX}af363dc8`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('lowercases an uppercase hex fingerprint', () => {
|
|
81
|
+
expect(certFingerprintId('AF363DC8')).toBe('fp:af363dc8');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('strips openssl-style colon separators', () => {
|
|
85
|
+
expect(certFingerprintId('AF:36:3D:C8')).toBe('fp:af363dc8');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('trims surrounding whitespace', () => {
|
|
89
|
+
expect(certFingerprintId(' af363dc8 ')).toBe('fp:af363dc8');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// certIdentityFromSubject — CertIdentity construction
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
describe('certIdentityFromSubject', () => {
|
|
98
|
+
it('builds an identity with the canonical DN and no fingerprint key', () => {
|
|
99
|
+
expect(certIdentityFromSubject('CN=alice')).toEqual({
|
|
100
|
+
subject: 'CN=alice',
|
|
101
|
+
canonicalDn: 'cn=alice',
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('includes the fingerprint when the platform supplied one', () => {
|
|
106
|
+
expect(certIdentityFromSubject('CN=alice', 'af363dc8')).toEqual({
|
|
107
|
+
subject: 'CN=alice',
|
|
108
|
+
canonicalDn: 'cn=alice',
|
|
109
|
+
fingerprint: 'af363dc8',
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('carries the canonical DN of a multi-RDN subject', () => {
|
|
114
|
+
const id = certIdentityFromSubject('O=Example,CN=alice');
|
|
115
|
+
expect(id.canonicalDn).toBe('cn=alice,o=Example');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { getLsString } from '../../src/caps/capabilities';
|
|
3
|
+
import { certIdentityFromSubject } from '../../src/certfp';
|
|
3
4
|
import { capReducer } from '../../src/commands/cap';
|
|
4
5
|
import { userReducer } from '../../src/commands/registration';
|
|
5
6
|
import { Effect } from '../../src/effects';
|
|
@@ -131,8 +132,12 @@ describe('capReducer — LS', () => {
|
|
|
131
132
|
|
|
132
133
|
it('advertises sasl=PLAIN,EXTERNAL when an mTLS provider is configured', () => {
|
|
133
134
|
const state = makeState();
|
|
134
|
-
|
|
135
|
-
const
|
|
135
|
+
state.secure = true;
|
|
136
|
+
const provider: MtlsIdentityProvider = {
|
|
137
|
+
getIdentity: () => certIdentityFromSubject('CN=alice'),
|
|
138
|
+
};
|
|
139
|
+
const cfg: ServerConfig = { ...serverConfig, sasl: { externalEnabled: true } };
|
|
140
|
+
const ctx = makeCtx(state, cfg, provider);
|
|
136
141
|
const out = capReducer(state, { command: 'CAP', params: ['LS'], tags: {} }, ctx);
|
|
137
142
|
const send = out.effects[0];
|
|
138
143
|
expect(send).toBeDefined();
|
|
@@ -142,6 +147,75 @@ describe('capReducer — LS', () => {
|
|
|
142
147
|
}
|
|
143
148
|
});
|
|
144
149
|
|
|
150
|
+
it('advertises sasl=PLAIN when EXTERNAL is not opted in, even with mTLS on a secure connection', () => {
|
|
151
|
+
// The operator opt-in gate: without sasl.externalEnabled the mechanism
|
|
152
|
+
// is not advertised, so clients never attempt an exchange the server
|
|
153
|
+
// would refuse with 908.
|
|
154
|
+
const state = makeState();
|
|
155
|
+
state.secure = true;
|
|
156
|
+
const provider: MtlsIdentityProvider = {
|
|
157
|
+
getIdentity: () => certIdentityFromSubject('CN=alice'),
|
|
158
|
+
};
|
|
159
|
+
const ctx = makeCtx(state, serverConfig, provider);
|
|
160
|
+
const out = capReducer(state, { command: 'CAP', params: ['LS'], tags: {} }, ctx);
|
|
161
|
+
const send = out.effects[0];
|
|
162
|
+
expect(send).toBeDefined();
|
|
163
|
+
if (send?.tag === 'Send') {
|
|
164
|
+
const body = send.lines[0]?.text ?? '';
|
|
165
|
+
expect(body).toContain('sasl=PLAIN');
|
|
166
|
+
expect(body).not.toContain('sasl=PLAIN,EXTERNAL');
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('advertises sasl=PLAIN on an insecure connection even when EXTERNAL is opted in', () => {
|
|
171
|
+
// SASL EXTERNAL is certificate-backed authentication: the client-cert
|
|
172
|
+
// identity only carries meaning over a transport the server trusts
|
|
173
|
+
// (TLS). A plaintext connection never sees the mechanism.
|
|
174
|
+
const state = makeState();
|
|
175
|
+
state.secure = false;
|
|
176
|
+
const provider: MtlsIdentityProvider = {
|
|
177
|
+
getIdentity: () => certIdentityFromSubject('CN=alice'),
|
|
178
|
+
};
|
|
179
|
+
const cfg: ServerConfig = { ...serverConfig, sasl: { externalEnabled: true } };
|
|
180
|
+
const ctx = makeCtx(state, cfg, provider);
|
|
181
|
+
const out = capReducer(state, { command: 'CAP', params: ['LS'], tags: {} }, ctx);
|
|
182
|
+
const send = out.effects[0];
|
|
183
|
+
expect(send).toBeDefined();
|
|
184
|
+
if (send?.tag === 'Send') {
|
|
185
|
+
const body = send.lines[0]?.text ?? '';
|
|
186
|
+
expect(body).toContain('sasl=PLAIN');
|
|
187
|
+
expect(body).not.toContain('sasl=PLAIN,EXTERNAL');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('advertises sasl=PLAIN when the transport-level EXTERNAL override is set, even with mTLS + opt-in on a secure connection', () => {
|
|
192
|
+
// A transport that cannot surface a client-cert identity (the
|
|
193
|
+
// Spectrum TCP container origin) pins the mechanism off via
|
|
194
|
+
// sasl.externalUnsupportedMessage: the cap must never advertise a
|
|
195
|
+
// mechanism AUTHENTICATE would refuse with a transport-specific 904.
|
|
196
|
+
const state = makeState();
|
|
197
|
+
state.secure = true;
|
|
198
|
+
const provider: MtlsIdentityProvider = {
|
|
199
|
+
getIdentity: () => certIdentityFromSubject('CN=alice'),
|
|
200
|
+
};
|
|
201
|
+
const cfg: ServerConfig = {
|
|
202
|
+
...serverConfig,
|
|
203
|
+
sasl: {
|
|
204
|
+
externalEnabled: true,
|
|
205
|
+
externalUnsupportedMessage: 'SASL EXTERNAL not supported on this transport',
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const ctx = makeCtx(state, cfg, provider);
|
|
209
|
+
const out = capReducer(state, { command: 'CAP', params: ['LS'], tags: {} }, ctx);
|
|
210
|
+
const send = out.effects[0];
|
|
211
|
+
expect(send).toBeDefined();
|
|
212
|
+
if (send?.tag === 'Send') {
|
|
213
|
+
const body = send.lines[0]?.text ?? '';
|
|
214
|
+
expect(body).toContain('sasl=PLAIN');
|
|
215
|
+
expect(body).not.toContain('sasl=PLAIN,EXTERNAL');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
145
219
|
it('advertises draft/multiline=4096 with the default byte budget', () => {
|
|
146
220
|
const state = makeState();
|
|
147
221
|
const ctx = makeCtx(state);
|
|
@@ -2,9 +2,11 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
CHANSERV_NICK,
|
|
4
4
|
DEFAULT_CHANNEL_LEVELS,
|
|
5
|
+
FOUNDER_RANK,
|
|
5
6
|
SHORTHAND_LEVELS,
|
|
6
7
|
chanservReducer,
|
|
7
8
|
positiveMlockLetters,
|
|
9
|
+
rankOf,
|
|
8
10
|
resolveMembershipTarget,
|
|
9
11
|
} from '../../src/commands/chanserv';
|
|
10
12
|
import { Effect } from '../../src/effects';
|
|
@@ -1670,6 +1672,29 @@ describe('SHORTHAND_LEVELS / DEFAULT_CHANNEL_LEVELS constants', () => {
|
|
|
1670
1672
|
});
|
|
1671
1673
|
});
|
|
1672
1674
|
|
|
1675
|
+
describe('rankOf / FOUNDER_RANK', () => {
|
|
1676
|
+
it('maps positive access levels to themselves', () => {
|
|
1677
|
+
expect(rankOf(SHORTHAND_LEVELS.VOP)).toBe(SHORTHAND_LEVELS.VOP);
|
|
1678
|
+
expect(rankOf(SHORTHAND_LEVELS.SOP)).toBe(SHORTHAND_LEVELS.SOP);
|
|
1679
|
+
expect(rankOf(7)).toBe(7);
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
it('clamps zero and negative levels to the minimum rank', () => {
|
|
1683
|
+
expect(rankOf(0)).toBe(0);
|
|
1684
|
+
expect(rankOf(-3)).toBe(0);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
it('passes the founder rank through unchanged', () => {
|
|
1688
|
+
expect(rankOf(FOUNDER_RANK)).toBe(Number.POSITIVE_INFINITY);
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
it('FOUNDER_RANK outranks every shorthand access level', () => {
|
|
1692
|
+
for (const level of Object.values(SHORTHAND_LEVELS)) {
|
|
1693
|
+
expect(FOUNDER_RANK).toBeGreaterThan(level);
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1673
1698
|
// ============================================================================
|
|
1674
1699
|
// OP / DEOP / VOICE / DEVOICE
|
|
1675
1700
|
// ============================================================================
|
|
@@ -1895,6 +1920,109 @@ describe('chanservReducer — DEOP', () => {
|
|
|
1895
1920
|
':ChanServ!ChanServ@services NOTICE mallory :Permission denied.',
|
|
1896
1921
|
);
|
|
1897
1922
|
});
|
|
1923
|
+
|
|
1924
|
+
it('rejects DEOP of the channel founder by an AUTOOP-level access entry', () => {
|
|
1925
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1926
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1927
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
1928
|
+
services.registerChannel('#home', 'alice');
|
|
1929
|
+
services.setChannelAccess('#home', 'carol', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
1930
|
+
const conn = makeConn('c1', 'carol');
|
|
1931
|
+
conn.account = 'carol';
|
|
1932
|
+
conn.userModes.registered = true;
|
|
1933
|
+
const ctx = makeCtx(conn, services);
|
|
1934
|
+
|
|
1935
|
+
const out = chanservReducer(conn, privmsg('DEOP #home alice'), ctx);
|
|
1936
|
+
|
|
1937
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
1938
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
1939
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1940
|
+
':ChanServ!ChanServ@services NOTICE carol :You do not have sufficient privileges on #home to affect alice.',
|
|
1941
|
+
);
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
it('allows peer-level DEOP between AUTOOP-level access entries', () => {
|
|
1945
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1946
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1947
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
1948
|
+
services.registerNick('dave', 'pw', 'd@example.com');
|
|
1949
|
+
services.registerChannel('#home', 'alice');
|
|
1950
|
+
services.setChannelAccess('#home', 'carol', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
1951
|
+
services.setChannelAccess('#home', 'dave', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
1952
|
+
const conn = makeConn('c1', 'carol');
|
|
1953
|
+
conn.account = 'carol';
|
|
1954
|
+
conn.userModes.registered = true;
|
|
1955
|
+
const ctx = makeCtx(conn, services);
|
|
1956
|
+
|
|
1957
|
+
const out = chanservReducer(conn, privmsg('DEOP #home dave'), ctx);
|
|
1958
|
+
|
|
1959
|
+
expect(firstBroadcast(out.effects)).toBeDefined();
|
|
1960
|
+
expect(firstApplyChannelDelta(out.effects)).toBeDefined();
|
|
1961
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1962
|
+
':ChanServ!ChanServ@services NOTICE carol :Set mode -o on dave on #home.',
|
|
1963
|
+
);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
it('rejects DEOP of a higher-ranked access entry by a lower-ranked caller', () => {
|
|
1967
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1968
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1969
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
1970
|
+
services.registerNick('dave', 'pw', 'd@example.com');
|
|
1971
|
+
services.registerChannel('#home', 'alice');
|
|
1972
|
+
services.setChannelAccess('#home', 'carol', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
1973
|
+
services.setChannelAccess('#home', 'dave', SHORTHAND_LEVELS.SOP);
|
|
1974
|
+
const conn = makeConn('c1', 'carol');
|
|
1975
|
+
conn.account = 'carol';
|
|
1976
|
+
conn.userModes.registered = true;
|
|
1977
|
+
const ctx = makeCtx(conn, services);
|
|
1978
|
+
|
|
1979
|
+
const out = chanservReducer(conn, privmsg('DEOP #home dave'), ctx);
|
|
1980
|
+
|
|
1981
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
1982
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
1983
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1984
|
+
':ChanServ!ChanServ@services NOTICE carol :You do not have sufficient privileges on #home to affect dave.',
|
|
1985
|
+
);
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
it('allows an SOP-level entry to DEOP an AOP-level target', () => {
|
|
1989
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1990
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1991
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
1992
|
+
services.registerNick('dave', 'pw', 'd@example.com');
|
|
1993
|
+
services.registerChannel('#home', 'alice');
|
|
1994
|
+
services.setChannelAccess('#home', 'carol', SHORTHAND_LEVELS.SOP);
|
|
1995
|
+
services.setChannelAccess('#home', 'dave', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
1996
|
+
const conn = makeConn('c1', 'carol');
|
|
1997
|
+
conn.account = 'carol';
|
|
1998
|
+
conn.userModes.registered = true;
|
|
1999
|
+
const ctx = makeCtx(conn, services);
|
|
2000
|
+
|
|
2001
|
+
const out = chanservReducer(conn, privmsg('DEOP #home dave'), ctx);
|
|
2002
|
+
|
|
2003
|
+
expect(firstBroadcast(out.effects)).toBeDefined();
|
|
2004
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2005
|
+
':ChanServ!ChanServ@services NOTICE carol :Set mode -o on dave on #home.',
|
|
2006
|
+
);
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
it('allows the founder to DEOP themselves', () => {
|
|
2010
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2011
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2012
|
+
services.registerChannel('#home', 'alice');
|
|
2013
|
+
const conn = makeConn();
|
|
2014
|
+
conn.account = 'alice';
|
|
2015
|
+
conn.userModes.registered = true;
|
|
2016
|
+
const ctx = makeCtx(conn, services);
|
|
2017
|
+
|
|
2018
|
+
const out = chanservReducer(conn, privmsg('DEOP #home alice'), ctx);
|
|
2019
|
+
|
|
2020
|
+
expect(firstBroadcast(out.effects)).toBeDefined();
|
|
2021
|
+
expect(firstApplyChannelDelta(out.effects)).toBeDefined();
|
|
2022
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2023
|
+
':ChanServ!ChanServ@services NOTICE alice :Set mode -o on alice on #home.',
|
|
2024
|
+
);
|
|
2025
|
+
});
|
|
1898
2026
|
});
|
|
1899
2027
|
|
|
1900
2028
|
describe('chanservReducer — VOICE', () => {
|
|
@@ -2076,6 +2204,44 @@ describe('chanservReducer — KICK', () => {
|
|
|
2076
2204
|
':ChanServ!ChanServ@services NOTICE alice :Syntax: KICK <#channel> <nick> [:<reason>].',
|
|
2077
2205
|
);
|
|
2078
2206
|
});
|
|
2207
|
+
|
|
2208
|
+
it('rejects KICK of the channel founder by an AUTOOP-level caller', () => {
|
|
2209
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2210
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2211
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
2212
|
+
services.registerChannel('#home', 'alice');
|
|
2213
|
+
services.setChannelAccess('#home', 'carol', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
2214
|
+
const conn = makeConn('c1', 'carol');
|
|
2215
|
+
conn.account = 'carol';
|
|
2216
|
+
conn.userModes.registered = true;
|
|
2217
|
+
const ctx = makeCtx(conn, services);
|
|
2218
|
+
|
|
2219
|
+
const out = chanservReducer(conn, privmsg('KICK #home alice'), ctx);
|
|
2220
|
+
|
|
2221
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
2222
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
2223
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2224
|
+
':ChanServ!ChanServ@services NOTICE carol :Permission denied.',
|
|
2225
|
+
);
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
it('allows the founder to KICK themselves (self-target through the rank guard)', () => {
|
|
2229
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2230
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2231
|
+
services.registerChannel('#home', 'alice');
|
|
2232
|
+
const conn = makeConn();
|
|
2233
|
+
conn.account = 'alice';
|
|
2234
|
+
conn.userModes.registered = true;
|
|
2235
|
+
const ctx = makeCtx(conn, services);
|
|
2236
|
+
|
|
2237
|
+
const out = chanservReducer(conn, privmsg('KICK #home alice'), ctx);
|
|
2238
|
+
|
|
2239
|
+
expect(firstBroadcast(out.effects)).toBeDefined();
|
|
2240
|
+
expect(firstApplyChannelDelta(out.effects)).toBeDefined();
|
|
2241
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2242
|
+
':ChanServ!ChanServ@services NOTICE alice :Kicked alice from #home.',
|
|
2243
|
+
);
|
|
2244
|
+
});
|
|
2079
2245
|
});
|
|
2080
2246
|
|
|
2081
2247
|
// ============================================================================
|
|
@@ -1013,6 +1013,146 @@ describe('chathistoryReducer — TAGMSG replay', () => {
|
|
|
1013
1013
|
});
|
|
1014
1014
|
});
|
|
1015
1015
|
|
|
1016
|
+
// ============================================================================
|
|
1017
|
+
// Limit ceiling (L9)
|
|
1018
|
+
//
|
|
1019
|
+
// The client-supplied limit is capped at a deployment-configurable ceiling
|
|
1020
|
+
// (ServerConfig.chathistory.maxLimit, default 100) so an oversized request
|
|
1021
|
+
// like `CHATHISTORY LATEST #chan 999999999` cannot ask a future higher-
|
|
1022
|
+
// capacity store to materialize a huge result set. The cap is silent: the
|
|
1023
|
+
// client simply receives at most the ceiling worth of replay lines.
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
|
|
1026
|
+
describe('chathistoryReducer — limit ceiling', () => {
|
|
1027
|
+
/** Number of replay lines inside the emitted BATCH frame (0 when elided). */
|
|
1028
|
+
function replayCount(effects: EffectType[]): number {
|
|
1029
|
+
if (effects.length === 0) return 0;
|
|
1030
|
+
const send = effects[0] as { lines: RawLine[] };
|
|
1031
|
+
return send.lines.length - 2; // minus BATCH start + BATCH end
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** Ctx over a store that retains more than the default ceiling. */
|
|
1035
|
+
function makeCappedCtx(conn: ConnectionState, store: MessageStore, config: ServerConfig): Ctx {
|
|
1036
|
+
return buildCtx({
|
|
1037
|
+
serverConfig: config,
|
|
1038
|
+
clock: new FakeClock(10_000),
|
|
1039
|
+
ids: new SequentialIdFactory(),
|
|
1040
|
+
motd: EmptyMotdProvider,
|
|
1041
|
+
messages: store,
|
|
1042
|
+
connection: conn,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
it('caps an oversized limit at the default ceiling of 100', () => {
|
|
1047
|
+
const store = new InMemoryMessageStore(1000);
|
|
1048
|
+
seed(store, '#foo', 150);
|
|
1049
|
+
const conn = makeConn();
|
|
1050
|
+
const chan = makeChan();
|
|
1051
|
+
chan.members.set('c1', { conn: 'c1', nick: 'alice', op: false, voice: false });
|
|
1052
|
+
const ctx = makeCappedCtx(conn, store, serverConfig);
|
|
1053
|
+
|
|
1054
|
+
const out = chathistoryReducer(
|
|
1055
|
+
chan,
|
|
1056
|
+
{ command: 'CHATHISTORY', params: ['LATEST', '#foo', '*', '999999999'], tags: {} },
|
|
1057
|
+
ctx,
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
expect(replayCount(out.effects)).toBe(100);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it('honors a limit at exactly the ceiling (100 passes through uncapped)', () => {
|
|
1064
|
+
const store = new InMemoryMessageStore(1000);
|
|
1065
|
+
seed(store, '#foo', 150);
|
|
1066
|
+
const conn = makeConn();
|
|
1067
|
+
const chan = makeChan();
|
|
1068
|
+
chan.members.set('c1', { conn: 'c1', nick: 'alice', op: false, voice: false });
|
|
1069
|
+
const ctx = makeCappedCtx(conn, store, serverConfig);
|
|
1070
|
+
|
|
1071
|
+
const out = chathistoryReducer(
|
|
1072
|
+
chan,
|
|
1073
|
+
{ command: 'CHATHISTORY', params: ['LATEST', '#foo', '*', '100'], tags: {} },
|
|
1074
|
+
ctx,
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
expect(replayCount(out.effects)).toBe(100);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it('passes a below-ceiling limit through unchanged', () => {
|
|
1081
|
+
const store = new InMemoryMessageStore(1000);
|
|
1082
|
+
seed(store, '#foo', 150);
|
|
1083
|
+
const conn = makeConn();
|
|
1084
|
+
const chan = makeChan();
|
|
1085
|
+
chan.members.set('c1', { conn: 'c1', nick: 'alice', op: false, voice: false });
|
|
1086
|
+
const ctx = makeCappedCtx(conn, store, serverConfig);
|
|
1087
|
+
|
|
1088
|
+
const out = chathistoryReducer(
|
|
1089
|
+
chan,
|
|
1090
|
+
{ command: 'CHATHISTORY', params: ['LATEST', '#foo', '*', '5'], tags: {} },
|
|
1091
|
+
ctx,
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
expect(replayCount(out.effects)).toBe(5);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it('caps at a configured lower ceiling via serverConfig.chathistory.maxLimit', () => {
|
|
1098
|
+
const store = new InMemoryMessageStore(1000);
|
|
1099
|
+
seed(store, '#foo', 150);
|
|
1100
|
+
const conn = makeConn();
|
|
1101
|
+
const chan = makeChan();
|
|
1102
|
+
chan.members.set('c1', { conn: 'c1', nick: 'alice', op: false, voice: false });
|
|
1103
|
+
const ctx = makeCappedCtx(conn, store, {
|
|
1104
|
+
...serverConfig,
|
|
1105
|
+
chathistory: { maxLimit: 3 },
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
const out = chathistoryReducer(
|
|
1109
|
+
chan,
|
|
1110
|
+
{ command: 'CHATHISTORY', params: ['LATEST', '#foo', '*', '999999999'], tags: {} },
|
|
1111
|
+
ctx,
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
expect(replayCount(out.effects)).toBe(3);
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
it('caps the default (omitted) limit at a configured lower ceiling', () => {
|
|
1118
|
+
const store = new InMemoryMessageStore(1000);
|
|
1119
|
+
seed(store, '#foo', 150);
|
|
1120
|
+
const conn = makeConn();
|
|
1121
|
+
const chan = makeChan();
|
|
1122
|
+
chan.members.set('c1', { conn: 'c1', nick: 'alice', op: false, voice: false });
|
|
1123
|
+
const ctx = makeCappedCtx(conn, store, {
|
|
1124
|
+
...serverConfig,
|
|
1125
|
+
chathistory: { maxLimit: 3 },
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
const out = chathistoryReducer(
|
|
1129
|
+
chan,
|
|
1130
|
+
// No limit param — the default 50 must still respect the ceiling of 3.
|
|
1131
|
+
{ command: 'CHATHISTORY', params: ['LATEST', '#foo', '*'], tags: {} },
|
|
1132
|
+
ctx,
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
expect(replayCount(out.effects)).toBe(3);
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it('returns 461 for a negative limit', () => {
|
|
1139
|
+
const store = new InMemoryMessageStore();
|
|
1140
|
+
seed(store, '#foo', 3);
|
|
1141
|
+
const conn = makeConn();
|
|
1142
|
+
const chan = makeChan();
|
|
1143
|
+
chan.members.set('c1', { conn: 'c1', nick: 'alice', op: false, voice: false });
|
|
1144
|
+
const ctx = makeCtx(conn, store);
|
|
1145
|
+
|
|
1146
|
+
const out = chathistoryReducer(
|
|
1147
|
+
chan,
|
|
1148
|
+
{ command: 'CHATHISTORY', params: ['LATEST', '#foo', '*', '-5'], tags: {} },
|
|
1149
|
+
ctx,
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
expect((out.effects[0] as { lines: RawLine[] }).lines[0]?.text).toContain('461');
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1016
1156
|
// ============================================================================
|
|
1017
1157
|
// No MessageStore bound (deployment disabled chathistory persistence)
|
|
1018
1158
|
// ============================================================================
|