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
|
@@ -3,6 +3,7 @@ import { namesReducer } from '../../src/commands/names';
|
|
|
3
3
|
import { Effect } from '../../src/effects';
|
|
4
4
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
5
|
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
6
|
+
import { byteLength } from '../../src/protocol/bytes';
|
|
6
7
|
import { type ChannelState, createChannel } from '../../src/state/channel';
|
|
7
8
|
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
8
9
|
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
@@ -238,6 +239,43 @@ describe('namesReducer — rejections', () => {
|
|
|
238
239
|
]);
|
|
239
240
|
});
|
|
240
241
|
|
|
242
|
+
it('emits 403 ERR_NOSUCHCHANNEL for a channel name containing NUL', () => {
|
|
243
|
+
const chan = makeChan('#foo');
|
|
244
|
+
const conn = makeConn();
|
|
245
|
+
const ctx = makeCtx(conn);
|
|
246
|
+
|
|
247
|
+
const out = namesReducer(chan, { command: 'NAMES', params: ['#foo\0bar'], tags: {} }, ctx);
|
|
248
|
+
|
|
249
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
250
|
+
Effect.send('c1', [L(':irc.example.com 403 alice #foo\0bar :No such channel')]),
|
|
251
|
+
]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('emits 403 ERR_NOSUCHCHANNEL for a channel name containing a control character', () => {
|
|
255
|
+
const chan = makeChan('#foo');
|
|
256
|
+
const conn = makeConn();
|
|
257
|
+
const ctx = makeCtx(conn);
|
|
258
|
+
|
|
259
|
+
const out = namesReducer(chan, { command: 'NAMES', params: ['#foo\x1fbar'], tags: {} }, ctx);
|
|
260
|
+
|
|
261
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
262
|
+
Effect.send('c1', [L(':irc.example.com 403 alice #foo\x1fbar :No such channel')]),
|
|
263
|
+
]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('emits 403 ERR_NOSUCHCHANNEL for a channel name whose UTF-8 byte size exceeds the cap', () => {
|
|
267
|
+
const long = `#${'é'.repeat(25)}`; // 51 UTF-8 bytes, only 26 UTF-16 units
|
|
268
|
+
const chan = makeChan(long);
|
|
269
|
+
const conn = makeConn();
|
|
270
|
+
const ctx = makeCtx(conn);
|
|
271
|
+
|
|
272
|
+
const out = namesReducer(chan, { command: 'NAMES', params: [long], tags: {} }, ctx);
|
|
273
|
+
|
|
274
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
275
|
+
Effect.send('c1', [L(`:irc.example.com 403 alice ${long} :No such channel`)]),
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
|
|
241
279
|
it('emits 403 ERR_NOSUCHCHANNEL for an empty channel name', () => {
|
|
242
280
|
const chan = makeChan('#foo');
|
|
243
281
|
const conn = makeConn();
|
|
@@ -340,3 +378,158 @@ describe('namesReducer — rejections', () => {
|
|
|
340
378
|
]);
|
|
341
379
|
});
|
|
342
380
|
});
|
|
381
|
+
|
|
382
|
+
// ============================================================================
|
|
383
|
+
// namesReducer — long NAMES list splitting (RFC 2812 §3.2.5)
|
|
384
|
+
// ============================================================================
|
|
385
|
+
|
|
386
|
+
/** RFC 1459 §2.3: 512-byte line budget minus the trailing CRLF pair. */
|
|
387
|
+
const MAX_TEXT_BYTES = 510;
|
|
388
|
+
|
|
389
|
+
describe('namesReducer — long NAMES list splitting', () => {
|
|
390
|
+
/** Returns the Send effect's lines, or [] when the first effect is not a Send. */
|
|
391
|
+
function sentLines(out: { effects: EffectType[] }): RawLine[] {
|
|
392
|
+
const send = out.effects[0];
|
|
393
|
+
return send?.tag === 'Send' ? send.lines : [];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Returns the trailing parameter of a formatted numeric line (after the last ` :`). */
|
|
397
|
+
function trailingOf(text: string): string {
|
|
398
|
+
const idx = text.lastIndexOf(' :');
|
|
399
|
+
return idx === -1 ? '' : text.slice(idx + 2);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Returns only the 353 RPL_NAMREPLY lines, in order. */
|
|
403
|
+
function namReplyLines(lines: RawLine[]): RawLine[] {
|
|
404
|
+
return lines.filter((l) => l.text.includes(' 353 '));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function pad(i: number): string {
|
|
408
|
+
return i.toString().padStart(3, '0');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
it('splits a 100-member roster across multiple complete 353 lines within the byte budget', () => {
|
|
412
|
+
const chan = makeChan('#foo');
|
|
413
|
+
const expected: string[] = [];
|
|
414
|
+
for (let i = 1; i <= 100; i++) {
|
|
415
|
+
const nick = `user${pad(i)}`;
|
|
416
|
+
addMember(chan, `c${i}`, nick);
|
|
417
|
+
expected.push(nick);
|
|
418
|
+
}
|
|
419
|
+
const conn = makeConn();
|
|
420
|
+
const ctx = makeCtx(conn);
|
|
421
|
+
|
|
422
|
+
const out = namesReducer(chan, { command: 'NAMES', params: ['#foo'], tags: {} }, ctx);
|
|
423
|
+
|
|
424
|
+
const lines = sentLines(out);
|
|
425
|
+
const replies = namReplyLines(lines);
|
|
426
|
+
expect(replies.length).toBeGreaterThan(1);
|
|
427
|
+
for (const line of replies) {
|
|
428
|
+
expect(byteLength(line.text)).toBeLessThanOrEqual(MAX_TEXT_BYTES);
|
|
429
|
+
}
|
|
430
|
+
// No mid-nick truncation: the roster survives intact, in order, across lines.
|
|
431
|
+
expect(replies.map((l) => trailingOf(l.text)).join(' ')).toBe(expected.join(' '));
|
|
432
|
+
// Exactly one 366 terminator, emitted after the final 353.
|
|
433
|
+
const endLines = lines.filter((l) => l.text.includes(' 366 '));
|
|
434
|
+
expect(endLines.length).toBe(1);
|
|
435
|
+
expect(lines[lines.length - 1]?.text).toBe(
|
|
436
|
+
':irc.example.com 366 alice #foo :End of /NAMES list.',
|
|
437
|
+
);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('counts @/+ prefix bytes in the budget: no non-final 353 could have taken the next member', () => {
|
|
441
|
+
const chan = makeChan('#foo');
|
|
442
|
+
const expected: string[] = [];
|
|
443
|
+
for (let i = 1; i <= 100; i++) {
|
|
444
|
+
const nick = `user${pad(i)}`;
|
|
445
|
+
// Alternating op (`@userNNN`, 8 bytes) and op+voice (`@+userNNN`, 9 bytes).
|
|
446
|
+
addMember(chan, `c${i}`, nick, true, i % 2 === 0);
|
|
447
|
+
expected.push(i % 2 === 0 ? `@+${nick}` : `@${nick}`);
|
|
448
|
+
}
|
|
449
|
+
const conn = makeConn();
|
|
450
|
+
conn.caps.add('multi-prefix');
|
|
451
|
+
const ctx = makeCtx(conn);
|
|
452
|
+
|
|
453
|
+
const out = namesReducer(chan, { command: 'NAMES', params: ['#foo'], tags: {} }, ctx);
|
|
454
|
+
|
|
455
|
+
const lines = sentLines(out);
|
|
456
|
+
const replies = namReplyLines(lines);
|
|
457
|
+
expect(replies.length).toBeGreaterThan(1);
|
|
458
|
+
for (const line of replies) {
|
|
459
|
+
expect(byteLength(line.text)).toBeLessThanOrEqual(MAX_TEXT_BYTES);
|
|
460
|
+
}
|
|
461
|
+
// Roster intact with prefixes.
|
|
462
|
+
expect(replies.map((l) => trailingOf(l.text)).join(' ')).toBe(expected.join(' '));
|
|
463
|
+
// Greedy maximality: appending the next line's first (prefixed) member to
|
|
464
|
+
// any non-final line would blow the budget — proof the prefix bytes were
|
|
465
|
+
// counted, not just the bare nicks.
|
|
466
|
+
for (let i = 0; i + 1 < replies.length; i++) {
|
|
467
|
+
const cur = replies[i]?.text ?? '';
|
|
468
|
+
const nextFirst = trailingOf(replies[i + 1]?.text ?? '').split(' ')[0] ?? '';
|
|
469
|
+
expect(nextFirst.length).toBeGreaterThan(0);
|
|
470
|
+
expect(byteLength(`${cur} ${nextFirst}`)).toBeGreaterThan(MAX_TEXT_BYTES);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('measures the budget in UTF-8 bytes: multi-byte nicks split even when UTF-16 units fit', () => {
|
|
475
|
+
const chan = makeChan('#foo');
|
|
476
|
+
const nick = 'é'.repeat(60); // 60 UTF-16 units but 120 UTF-8 bytes per nick
|
|
477
|
+
for (let i = 1; i <= 7; i++) {
|
|
478
|
+
addMember(chan, `c${i}`, nick);
|
|
479
|
+
}
|
|
480
|
+
const conn = makeConn();
|
|
481
|
+
const ctx = makeCtx(conn);
|
|
482
|
+
|
|
483
|
+
const out = namesReducer(chan, { command: 'NAMES', params: ['#foo'], tags: {} }, ctx);
|
|
484
|
+
|
|
485
|
+
const lines = sentLines(out);
|
|
486
|
+
const replies = namReplyLines(lines);
|
|
487
|
+
// Names alone: 7*120 + 6 = 846 bytes (but only 426 UTF-16 units) — a
|
|
488
|
+
// code-unit-based packer would emit one 881-byte line.
|
|
489
|
+
expect(replies.length).toBeGreaterThan(1);
|
|
490
|
+
for (const line of replies) {
|
|
491
|
+
expect(byteLength(line.text)).toBeLessThanOrEqual(MAX_TEXT_BYTES);
|
|
492
|
+
}
|
|
493
|
+
expect(replies.map((l) => trailingOf(l.text)).join(' ')).toBe(
|
|
494
|
+
Array<string>(7).fill(nick).join(' '),
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('emits exactly one 353 with an empty trailing + the 366 for a zero-member channel', () => {
|
|
499
|
+
const chan = makeChan('#foo'); // roster is completely empty
|
|
500
|
+
const conn = makeConn(); // requester is not a member either
|
|
501
|
+
const ctx = makeCtx(conn);
|
|
502
|
+
|
|
503
|
+
const out = namesReducer(chan, { command: 'NAMES', params: ['#foo'], tags: {} }, ctx);
|
|
504
|
+
|
|
505
|
+
expect(sentLines(out)).toEqual<RawLine[]>([
|
|
506
|
+
L(':irc.example.com 353 alice = #foo :'),
|
|
507
|
+
L(':irc.example.com 366 alice #foo :End of /NAMES list.'),
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('wraps every split 353 line plus the 366 inside one BATCH frame when batch is negotiated', () => {
|
|
512
|
+
const chan = makeChan('#foo');
|
|
513
|
+
for (let i = 1; i <= 100; i++) {
|
|
514
|
+
addMember(chan, `c${i}`, `user${pad(i)}`);
|
|
515
|
+
}
|
|
516
|
+
const conn = makeConn();
|
|
517
|
+
conn.caps.add('batch');
|
|
518
|
+
const ctx = makeCtx(conn);
|
|
519
|
+
|
|
520
|
+
const out = namesReducer(chan, { command: 'NAMES', params: ['#foo'], tags: {} }, ctx);
|
|
521
|
+
|
|
522
|
+
const lines = sentLines(out);
|
|
523
|
+
expect(lines[0]?.text).toBe('BATCH +batch-0 names');
|
|
524
|
+
expect(lines[lines.length - 1]?.text).toBe('BATCH -batch-0');
|
|
525
|
+
const inner = lines.slice(1, -1);
|
|
526
|
+
const replies = namReplyLines(inner);
|
|
527
|
+
expect(replies.length).toBeGreaterThan(1);
|
|
528
|
+
for (const line of replies) {
|
|
529
|
+
expect(byteLength(line.text)).toBeLessThanOrEqual(MAX_TEXT_BYTES);
|
|
530
|
+
}
|
|
531
|
+
expect(inner[inner.length - 1]?.text).toBe(
|
|
532
|
+
':irc.example.com 366 alice #foo :End of /NAMES list.',
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { NICKSERV_NICK, enforceRegisteredNick, nickservReducer } from '../../src/commands/nickserv';
|
|
3
3
|
import { Effect } from '../../src/effects';
|
|
4
4
|
import type { Effect as EffectType } from '../../src/effects';
|
|
5
|
-
import type { ServicesStore } from '../../src/ports';
|
|
5
|
+
import type { ServicesAuthResult, ServicesStore } from '../../src/ports';
|
|
6
6
|
import {
|
|
7
7
|
EmptyMotdProvider,
|
|
8
8
|
FakeClock,
|
|
@@ -41,9 +41,10 @@ function makeCtx(
|
|
|
41
41
|
state: ConnectionState,
|
|
42
42
|
services?: ServicesStore,
|
|
43
43
|
clock = new FakeClock(NOW),
|
|
44
|
+
configOverrides: Partial<ServerConfig> = {},
|
|
44
45
|
): Ctx {
|
|
45
46
|
return buildCtx({
|
|
46
|
-
serverConfig,
|
|
47
|
+
serverConfig: { ...serverConfig, ...configOverrides },
|
|
47
48
|
clock,
|
|
48
49
|
ids: new SequentialIdFactory(),
|
|
49
50
|
motd: EmptyMotdProvider,
|
|
@@ -283,6 +284,185 @@ describe('nickservReducer — IDENTIFY', () => {
|
|
|
283
284
|
});
|
|
284
285
|
});
|
|
285
286
|
|
|
287
|
+
/**
|
|
288
|
+
* InMemoryServicesStore subclass that counts `verifyNick` calls so the
|
|
289
|
+
* identify-freeze tests can assert the frozen attempt is rejected BEFORE
|
|
290
|
+
* scrypt runs (the freeze must short-circuit the verification path).
|
|
291
|
+
*/
|
|
292
|
+
class VerifyCountingStore extends InMemoryServicesStore {
|
|
293
|
+
verifyCalls = 0;
|
|
294
|
+
override verifyNick(nick: string, password: string): ServicesAuthResult {
|
|
295
|
+
this.verifyCalls++;
|
|
296
|
+
return super.verifyNick(nick, password);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
describe('nickservReducer — IDENTIFY freeze (per-account throttle)', () => {
|
|
301
|
+
it('short-circuits the 6th failed IDENTIFY with a freeze notice (no scrypt)', () => {
|
|
302
|
+
const clock = new FakeClock(NOW);
|
|
303
|
+
const services = new VerifyCountingStore({ clock });
|
|
304
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
305
|
+
const conn = makeConn();
|
|
306
|
+
const ctx = makeCtx(conn, services, clock);
|
|
307
|
+
|
|
308
|
+
for (let i = 0; i < 5; i++) {
|
|
309
|
+
nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
310
|
+
}
|
|
311
|
+
const out = nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
312
|
+
|
|
313
|
+
expect(services.verifyCalls).toBe(5);
|
|
314
|
+
expect(sentTexts(out.effects)).toContain(
|
|
315
|
+
':NickServ!NickServ@services NOTICE alice :Nick alice is temporarily frozen due to too many failed identify attempts.',
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('lifts the freeze once the failures age out of the 300s window', () => {
|
|
320
|
+
const clock = new FakeClock(NOW);
|
|
321
|
+
const services = new VerifyCountingStore({ clock });
|
|
322
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
323
|
+
const conn = makeConn();
|
|
324
|
+
const ctx = makeCtx(conn, services, clock);
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < 5; i++) {
|
|
327
|
+
nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
328
|
+
}
|
|
329
|
+
// Exactly at the window boundary the failures have aged out.
|
|
330
|
+
clock.advance(300_000);
|
|
331
|
+
const out = nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
332
|
+
|
|
333
|
+
expect(services.verifyCalls).toBe(6);
|
|
334
|
+
expect(sentTexts(out.effects)).toContain(
|
|
335
|
+
':NickServ!NickServ@services NOTICE alice :Invalid password.',
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('keeps the freeze while the failures are still inside the window', () => {
|
|
340
|
+
const clock = new FakeClock(NOW);
|
|
341
|
+
const services = new VerifyCountingStore({ clock });
|
|
342
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
343
|
+
const conn = makeConn();
|
|
344
|
+
const ctx = makeCtx(conn, services, clock);
|
|
345
|
+
|
|
346
|
+
for (let i = 0; i < 5; i++) {
|
|
347
|
+
nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
348
|
+
}
|
|
349
|
+
clock.advance(300_000 - 1);
|
|
350
|
+
const out = nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
351
|
+
|
|
352
|
+
expect(services.verifyCalls).toBe(5);
|
|
353
|
+
expect(sentTexts(out.effects)).toContain(
|
|
354
|
+
':NickServ!NickServ@services NOTICE alice :Nick alice is temporarily frozen due to too many failed identify attempts.',
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('delivers a queued owner NOTICE on the next successful identify after the freeze', () => {
|
|
359
|
+
const clock = new FakeClock(NOW);
|
|
360
|
+
const services = new VerifyCountingStore({ clock });
|
|
361
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
362
|
+
const conn = makeConn();
|
|
363
|
+
const ctx = makeCtx(conn, services, clock);
|
|
364
|
+
|
|
365
|
+
for (let i = 0; i < 5; i++) {
|
|
366
|
+
nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
367
|
+
}
|
|
368
|
+
clock.advance(300_000);
|
|
369
|
+
const out = nickservReducer(conn, privmsg('IDENTIFY hunter2'), ctx);
|
|
370
|
+
|
|
371
|
+
expect(conn.account).toBe('alice');
|
|
372
|
+
expect(sentTexts(out.effects)).toContain(
|
|
373
|
+
':NickServ!NickServ@services NOTICE alice :Warning: 5 failed identify attempts recorded against your account since your last successful login.',
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// The notice is delivered once — a second successful identify (fresh
|
|
377
|
+
// connection) does not repeat it.
|
|
378
|
+
const conn2 = makeConn('c2', 'alice');
|
|
379
|
+
const ctx2 = makeCtx(conn2, services, clock);
|
|
380
|
+
const out2 = nickservReducer(conn2, privmsg('IDENTIFY hunter2'), ctx2);
|
|
381
|
+
expect(sentTexts(out2.effects).some((t) => t.includes('Warning:'))).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('delivers the owner NOTICE with singular wording for a single failed identify', () => {
|
|
385
|
+
const clock = new FakeClock(NOW);
|
|
386
|
+
const services = new VerifyCountingStore({ clock });
|
|
387
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
388
|
+
const conn = makeConn();
|
|
389
|
+
const ctx = makeCtx(conn, services, clock);
|
|
390
|
+
|
|
391
|
+
nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
392
|
+
const out = nickservReducer(conn, privmsg('IDENTIFY hunter2'), ctx);
|
|
393
|
+
|
|
394
|
+
expect(sentTexts(out.effects)).toContain(
|
|
395
|
+
':NickServ!NickServ@services NOTICE alice :Warning: 1 failed identify attempt recorded against your account since your last successful login.',
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('resets the failure counter on a successful identify', () => {
|
|
400
|
+
const clock = new FakeClock(NOW);
|
|
401
|
+
const services = new VerifyCountingStore({ clock });
|
|
402
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
403
|
+
const conn = makeConn();
|
|
404
|
+
const ctx = makeCtx(conn, services, clock);
|
|
405
|
+
|
|
406
|
+
for (let i = 0; i < 3; i++) {
|
|
407
|
+
nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
408
|
+
}
|
|
409
|
+
nickservReducer(conn, privmsg('IDENTIFY hunter2'), ctx);
|
|
410
|
+
|
|
411
|
+
// Fresh connection (the first is now identified). Without a reset the
|
|
412
|
+
// counter would still hold 3 failures and the account would freeze
|
|
413
|
+
// after only 2 more; with the reset all 5 new attempts must reach
|
|
414
|
+
// scrypt and return the plain invalid-password notice.
|
|
415
|
+
const conn2 = makeConn('c2', 'alice');
|
|
416
|
+
const ctx2 = makeCtx(conn2, services, clock);
|
|
417
|
+
for (let i = 0; i < 5; i++) {
|
|
418
|
+
const out = nickservReducer(conn2, privmsg('IDENTIFY wrongpass'), ctx2);
|
|
419
|
+
expect(sentTexts(out.effects)).toContain(
|
|
420
|
+
':NickServ!NickServ@services NOTICE alice :Invalid password.',
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
expect(services.verifyCalls).toBe(3 + 1 + 5);
|
|
424
|
+
|
|
425
|
+
// The 6th new failure crosses the (reset) threshold → frozen.
|
|
426
|
+
const frozen = nickservReducer(conn2, privmsg('IDENTIFY wrongpass'), ctx2);
|
|
427
|
+
expect(services.verifyCalls).toBe(9);
|
|
428
|
+
expect(sentTexts(frozen.effects)).toContain(
|
|
429
|
+
':NickServ!NickServ@services NOTICE alice :Nick alice is temporarily frozen due to too many failed identify attempts.',
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('honours nickServ.maxIdentifyFailures and identifyFreezeSeconds overrides', () => {
|
|
434
|
+
const clock = new FakeClock(NOW);
|
|
435
|
+
const services = new VerifyCountingStore({ clock });
|
|
436
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
437
|
+
const conn = makeConn();
|
|
438
|
+
const ctx = makeCtx(conn, services, clock, {
|
|
439
|
+
nickServ: { maxIdentifyFailures: 2, identifyFreezeSeconds: 60 },
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
443
|
+
const second = nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
444
|
+
expect(services.verifyCalls).toBe(2);
|
|
445
|
+
expect(sentTexts(second.effects)).toContain(
|
|
446
|
+
':NickServ!NickServ@services NOTICE alice :Invalid password.',
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// Threshold 2 reached → the next attempt is frozen without scrypt.
|
|
450
|
+
const frozen = nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
451
|
+
expect(services.verifyCalls).toBe(2);
|
|
452
|
+
expect(sentTexts(frozen.effects)).toContain(
|
|
453
|
+
':NickServ!NickServ@services NOTICE alice :Nick alice is temporarily frozen due to too many failed identify attempts.',
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// The configured 60s window ages the failures out sooner than default.
|
|
457
|
+
clock.advance(60_000);
|
|
458
|
+
const after = nickservReducer(conn, privmsg('IDENTIFY wrongpass'), ctx);
|
|
459
|
+
expect(services.verifyCalls).toBe(3);
|
|
460
|
+
expect(sentTexts(after.effects)).toContain(
|
|
461
|
+
':NickServ!NickServ@services NOTICE alice :Invalid password.',
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
286
466
|
describe('nickservReducer — ID (alias for IDENTIFY)', () => {
|
|
287
467
|
it('sets +r and account on correct password (ID <password>)', () => {
|
|
288
468
|
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
@@ -622,7 +802,7 @@ describe('nickservReducer — misc', () => {
|
|
|
622
802
|
const out = nickservReducer(conn, privmsg('FROBNICATE foo bar'), ctx);
|
|
623
803
|
|
|
624
804
|
expect(sentTexts(out.effects)).toContain(
|
|
625
|
-
':NickServ!NickServ@services NOTICE alice :Unknown command. Available: REGISTER, IDENTIFY, DROP, INFO, SET',
|
|
805
|
+
':NickServ!NickServ@services NOTICE alice :Unknown command. Available: REGISTER, IDENTIFY, DROP, INFO, SET (ENFORCE, PASSWORD)',
|
|
626
806
|
);
|
|
627
807
|
});
|
|
628
808
|
|
|
@@ -857,6 +1037,242 @@ describe('nickservReducer — SET ENFORCE', () => {
|
|
|
857
1037
|
});
|
|
858
1038
|
});
|
|
859
1039
|
|
|
1040
|
+
// ============================================================================
|
|
1041
|
+
// SET PASSWORD
|
|
1042
|
+
// ============================================================================
|
|
1043
|
+
|
|
1044
|
+
describe('nickservReducer — SET PASSWORD', () => {
|
|
1045
|
+
it('changes the password when the caller supplies the correct old one', () => {
|
|
1046
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1047
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1048
|
+
const conn = makeConn();
|
|
1049
|
+
conn.account = 'alice';
|
|
1050
|
+
conn.userModes.registered = true;
|
|
1051
|
+
const ctx = makeCtx(conn, services);
|
|
1052
|
+
|
|
1053
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
1054
|
+
|
|
1055
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1056
|
+
':NickServ!NickServ@services NOTICE alice :Password changed.',
|
|
1057
|
+
);
|
|
1058
|
+
// The new password verifies, the old one no longer does.
|
|
1059
|
+
expect(services.verifyNick('alice', 'newpassword').ok).toBe(true);
|
|
1060
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(false);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it('rejects a wrong old password and leaves the credential unchanged', () => {
|
|
1064
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1065
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1066
|
+
const conn = makeConn();
|
|
1067
|
+
conn.account = 'alice';
|
|
1068
|
+
conn.userModes.registered = true;
|
|
1069
|
+
const ctx = makeCtx(conn, services);
|
|
1070
|
+
|
|
1071
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD wrongold newpassword'), ctx);
|
|
1072
|
+
|
|
1073
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1074
|
+
':NickServ!NickServ@services NOTICE alice :Invalid password.',
|
|
1075
|
+
);
|
|
1076
|
+
// The original password still verifies; the new one does not.
|
|
1077
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
1078
|
+
expect(services.verifyNick('alice', 'newpassword').ok).toBe(false);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('requires identification before SET PASSWORD (unidentified caller)', () => {
|
|
1082
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1083
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1084
|
+
const conn = makeConn();
|
|
1085
|
+
// No account / +r.
|
|
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 :You must identify before changing your password.',
|
|
1092
|
+
);
|
|
1093
|
+
// Credential unchanged.
|
|
1094
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
1095
|
+
expect(services.verifyNick('alice', 'newpassword').ok).toBe(false);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it('rejects SET PASSWORD when identified as a different account', () => {
|
|
1099
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1100
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1101
|
+
// bob is identified, but is using alice's nick.
|
|
1102
|
+
services.registerNick('bob', 'bobpw', 'bob@example.com');
|
|
1103
|
+
const conn = makeConn('c1', 'alice');
|
|
1104
|
+
conn.account = 'bob';
|
|
1105
|
+
conn.userModes.registered = true;
|
|
1106
|
+
const ctx = makeCtx(conn, services);
|
|
1107
|
+
|
|
1108
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
1109
|
+
|
|
1110
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1111
|
+
':NickServ!NickServ@services NOTICE alice :You must identify before changing your password.',
|
|
1112
|
+
);
|
|
1113
|
+
// Credential unchanged.
|
|
1114
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
1115
|
+
expect(services.verifyNick('alice', 'newpassword').ok).toBe(false);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
it('emits the syntax notice when args are missing', () => {
|
|
1119
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1120
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1121
|
+
const conn = makeConn();
|
|
1122
|
+
conn.account = 'alice';
|
|
1123
|
+
conn.userModes.registered = true;
|
|
1124
|
+
const ctx = makeCtx(conn, services);
|
|
1125
|
+
|
|
1126
|
+
const noArgs = nickservReducer(conn, privmsg('SET PASSWORD'), ctx);
|
|
1127
|
+
expect(sentTexts(noArgs.effects)).toContain(
|
|
1128
|
+
':NickServ!NickServ@services NOTICE alice :Syntax: SET PASSWORD <old-password> <new-password>',
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
const oneArg = nickservReducer(conn, privmsg('SET PASSWORD only-one'), ctx);
|
|
1132
|
+
expect(sentTexts(oneArg.effects)).toContain(
|
|
1133
|
+
':NickServ!NickServ@services NOTICE alice :Syntax: SET PASSWORD <old-password> <new-password>',
|
|
1134
|
+
);
|
|
1135
|
+
// Credential unchanged.
|
|
1136
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('rejects a new password shorter than the default minimum (8)', () => {
|
|
1140
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1141
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1142
|
+
const conn = makeConn();
|
|
1143
|
+
conn.account = 'alice';
|
|
1144
|
+
conn.userModes.registered = true;
|
|
1145
|
+
const ctx = makeCtx(conn, services);
|
|
1146
|
+
|
|
1147
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 short'), ctx);
|
|
1148
|
+
|
|
1149
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1150
|
+
':NickServ!NickServ@services NOTICE alice :Password is too short (minimum 8 characters).',
|
|
1151
|
+
);
|
|
1152
|
+
// Credential unchanged.
|
|
1153
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
1154
|
+
expect(services.verifyNick('alice', 'short').ok).toBe(false);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it('honours a configured nickServ.minPasswordLength override', () => {
|
|
1158
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1159
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1160
|
+
const conn = makeConn();
|
|
1161
|
+
conn.account = 'alice';
|
|
1162
|
+
conn.userModes.registered = true;
|
|
1163
|
+
const ctx: Ctx = buildCtx({
|
|
1164
|
+
serverConfig: { ...serverConfig, nickServ: { minPasswordLength: 12 } },
|
|
1165
|
+
clock: new FakeClock(NOW),
|
|
1166
|
+
ids: new SequentialIdFactory(),
|
|
1167
|
+
motd: EmptyMotdProvider,
|
|
1168
|
+
connection: conn,
|
|
1169
|
+
services,
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 elevenchars'), ctx);
|
|
1173
|
+
|
|
1174
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1175
|
+
':NickServ!NickServ@services NOTICE alice :Password is too short (minimum 12 characters).',
|
|
1176
|
+
);
|
|
1177
|
+
// Credential unchanged.
|
|
1178
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
it('rejects a new password longer than the maximum (256)', () => {
|
|
1182
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1183
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1184
|
+
const conn = makeConn();
|
|
1185
|
+
conn.account = 'alice';
|
|
1186
|
+
conn.userModes.registered = true;
|
|
1187
|
+
const ctx = makeCtx(conn, services);
|
|
1188
|
+
|
|
1189
|
+
const tooLong = 'x'.repeat(257);
|
|
1190
|
+
const out = nickservReducer(conn, privmsg(`SET PASSWORD hunter2 ${tooLong}`), ctx);
|
|
1191
|
+
|
|
1192
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1193
|
+
':NickServ!NickServ@services NOTICE alice :Password is too long (maximum 256 characters).',
|
|
1194
|
+
);
|
|
1195
|
+
// Credential unchanged.
|
|
1196
|
+
expect(services.verifyNick('alice', 'hunter2').ok).toBe(true);
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('keeps the session +r after a successful change (no re-identify required)', () => {
|
|
1200
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1201
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1202
|
+
const conn = makeConn();
|
|
1203
|
+
conn.account = 'alice';
|
|
1204
|
+
conn.userModes.registered = true;
|
|
1205
|
+
const ctx = makeCtx(conn, services);
|
|
1206
|
+
|
|
1207
|
+
nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
1208
|
+
|
|
1209
|
+
expect(conn.userModes.registered).toBe(true);
|
|
1210
|
+
expect(conn.account).toBe('alice');
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it('accepts a new password exactly at the minimum length boundary (8)', () => {
|
|
1214
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1215
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1216
|
+
const conn = makeConn();
|
|
1217
|
+
conn.account = 'alice';
|
|
1218
|
+
conn.userModes.registered = true;
|
|
1219
|
+
const ctx = makeCtx(conn, services);
|
|
1220
|
+
|
|
1221
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 exactly8'), ctx);
|
|
1222
|
+
|
|
1223
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1224
|
+
':NickServ!NickServ@services NOTICE alice :Password changed.',
|
|
1225
|
+
);
|
|
1226
|
+
expect(services.verifyNick('alice', 'exactly8').ok).toBe(true);
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
it('accepts a new password exactly at the maximum length boundary (256)', () => {
|
|
1230
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1231
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1232
|
+
const conn = makeConn();
|
|
1233
|
+
conn.account = 'alice';
|
|
1234
|
+
conn.userModes.registered = true;
|
|
1235
|
+
const ctx = makeCtx(conn, services);
|
|
1236
|
+
|
|
1237
|
+
const boundary = 'x'.repeat(256);
|
|
1238
|
+
const out = nickservReducer(conn, privmsg(`SET PASSWORD hunter2 ${boundary}`), ctx);
|
|
1239
|
+
|
|
1240
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1241
|
+
':NickServ!NickServ@services NOTICE alice :Password changed.',
|
|
1242
|
+
);
|
|
1243
|
+
expect(services.verifyNick('alice', boundary).ok).toBe(true);
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
it('is case-insensitive on the PASSWORD subkey', () => {
|
|
1247
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1248
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1249
|
+
const conn = makeConn();
|
|
1250
|
+
conn.account = 'alice';
|
|
1251
|
+
conn.userModes.registered = true;
|
|
1252
|
+
const ctx = makeCtx(conn, services);
|
|
1253
|
+
|
|
1254
|
+
const out = nickservReducer(conn, privmsg('set password hunter2 newpassword'), ctx);
|
|
1255
|
+
|
|
1256
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1257
|
+
':NickServ!NickServ@services NOTICE alice :Password changed.',
|
|
1258
|
+
);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('rejects SET PASSWORD on an unregistered nick', () => {
|
|
1262
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1263
|
+
const conn = makeConn();
|
|
1264
|
+
conn.account = 'alice';
|
|
1265
|
+
conn.userModes.registered = true;
|
|
1266
|
+
const ctx = makeCtx(conn, services);
|
|
1267
|
+
|
|
1268
|
+
const out = nickservReducer(conn, privmsg('SET PASSWORD hunter2 newpassword'), ctx);
|
|
1269
|
+
|
|
1270
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1271
|
+
':NickServ!NickServ@services NOTICE alice :Nick alice is not registered.',
|
|
1272
|
+
);
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
|
|
860
1276
|
// ============================================================================
|
|
861
1277
|
// enforceRegisteredNick — the enforcement hook fired by the NICK path
|
|
862
1278
|
// ============================================================================
|