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
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
DEFAULT_CREATED_TEXT,
|
|
5
5
|
ServerConfigSchema,
|
|
6
6
|
formatCreatedAt,
|
|
7
|
+
narrowOperCred,
|
|
7
8
|
parseServerConfig,
|
|
8
9
|
resolveServerVersion,
|
|
9
10
|
} from '../src/config';
|
|
@@ -327,7 +328,8 @@ describe('ServerConfigSchema — operCreds', () => {
|
|
|
327
328
|
});
|
|
328
329
|
expect(cfg.operCreds).toHaveLength(2);
|
|
329
330
|
expect(cfg.operCreds[0]?.user).toBe('op1');
|
|
330
|
-
|
|
331
|
+
const second = cfg.operCreds[1];
|
|
332
|
+
expect(second !== undefined && 'password' in second ? second.password : undefined).toBe('p2');
|
|
331
333
|
});
|
|
332
334
|
|
|
333
335
|
it('defaults operCreds to an empty array when omitted', () => {
|
|
@@ -384,6 +386,120 @@ describe('ServerConfigSchema — operCreds', () => {
|
|
|
384
386
|
}),
|
|
385
387
|
).toThrowError(/operCreds/u);
|
|
386
388
|
});
|
|
389
|
+
|
|
390
|
+
it('accepts the new hashed form {user, salt, hash}', () => {
|
|
391
|
+
const cfg = parseServerConfig({
|
|
392
|
+
serverName: 's',
|
|
393
|
+
networkName: 'n',
|
|
394
|
+
operCreds: [{ user: 'admin', salt: 'c2FsdA==', hash: 'aGFzaA==' }],
|
|
395
|
+
});
|
|
396
|
+
expect(cfg.operCreds).toHaveLength(1);
|
|
397
|
+
expect(cfg.operCreds[0]).toEqual({
|
|
398
|
+
user: 'admin',
|
|
399
|
+
salt: 'c2FsdA==',
|
|
400
|
+
hash: 'aGFzaA==',
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('accepts a mix of legacy plaintext and hashed creds', () => {
|
|
405
|
+
const cfg = parseServerConfig({
|
|
406
|
+
serverName: 's',
|
|
407
|
+
networkName: 'n',
|
|
408
|
+
operCreds: [
|
|
409
|
+
{ user: 'legacy', password: 'p' },
|
|
410
|
+
{ user: 'hashed', salt: 'c2FsdA==', hash: 'aGFzaA==' },
|
|
411
|
+
],
|
|
412
|
+
});
|
|
413
|
+
expect(cfg.operCreds).toHaveLength(2);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('rejects a hashed cred missing salt', () => {
|
|
417
|
+
expect(() =>
|
|
418
|
+
parseServerConfig({
|
|
419
|
+
serverName: 's',
|
|
420
|
+
networkName: 'n',
|
|
421
|
+
operCreds: [{ user: 'op', hash: 'aGFzaA==' }],
|
|
422
|
+
}),
|
|
423
|
+
).toThrowError(/salt/u);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('rejects a hashed cred missing hash', () => {
|
|
427
|
+
expect(() =>
|
|
428
|
+
parseServerConfig({
|
|
429
|
+
serverName: 's',
|
|
430
|
+
networkName: 'n',
|
|
431
|
+
operCreds: [{ user: 'op', salt: 'c2FsdA==' }],
|
|
432
|
+
}),
|
|
433
|
+
).toThrowError(/hash/u);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('rejects a hashed cred with empty salt', () => {
|
|
437
|
+
expect(() =>
|
|
438
|
+
parseServerConfig({
|
|
439
|
+
serverName: 's',
|
|
440
|
+
networkName: 'n',
|
|
441
|
+
operCreds: [{ user: 'op', salt: '', hash: 'aGFzaA==' }],
|
|
442
|
+
}),
|
|
443
|
+
).toThrowError(/salt/u);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('rejects a hashed cred with empty hash', () => {
|
|
447
|
+
expect(() =>
|
|
448
|
+
parseServerConfig({
|
|
449
|
+
serverName: 's',
|
|
450
|
+
networkName: 'n',
|
|
451
|
+
operCreds: [{ user: 'op', salt: 'c2FsdA==', hash: '' }],
|
|
452
|
+
}),
|
|
453
|
+
).toThrowError(/hash/u);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('rejects a cred that mixes plaintext password and hashed salt/hash', () => {
|
|
457
|
+
expect(() =>
|
|
458
|
+
parseServerConfig({
|
|
459
|
+
serverName: 's',
|
|
460
|
+
networkName: 'n',
|
|
461
|
+
operCreds: [{ user: 'op', password: 'p', salt: 'c2FsdA==', hash: 'aGFzaA==' }],
|
|
462
|
+
}),
|
|
463
|
+
).toThrowError(/password/u);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('rejects a cred that mixes plaintext password with only salt', () => {
|
|
467
|
+
expect(() =>
|
|
468
|
+
parseServerConfig({
|
|
469
|
+
serverName: 's',
|
|
470
|
+
networkName: 'n',
|
|
471
|
+
operCreds: [{ user: 'op', password: 'p', salt: 'c2FsdA==' }],
|
|
472
|
+
}),
|
|
473
|
+
).toThrowError(/password/u);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ============================================================================
|
|
478
|
+
// narrowOperCred — loose parse output → OperCred union narrowing
|
|
479
|
+
// ============================================================================
|
|
480
|
+
|
|
481
|
+
describe('narrowOperCred', () => {
|
|
482
|
+
it('narrows to the plaintext shape when password is present', () => {
|
|
483
|
+
expect(narrowOperCred({ user: 'op', password: 'p' })).toEqual({
|
|
484
|
+
user: 'op',
|
|
485
|
+
password: 'p',
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('narrows to the hashed shape when salt + hash are present', () => {
|
|
490
|
+
expect(narrowOperCred({ user: 'op', salt: 's', hash: 'h' })).toEqual({
|
|
491
|
+
user: 'op',
|
|
492
|
+
salt: 's',
|
|
493
|
+
hash: 'h',
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('is total: a bare {user} input falls back to empty salt/hash (defensive only)', () => {
|
|
498
|
+
// Unreachable through parseServerConfig (the superRefine rejects a
|
|
499
|
+
// cred with no authentication fields); asserted so the defensive
|
|
500
|
+
// fallback branch is covered and the helper is total.
|
|
501
|
+
expect(narrowOperCred({ user: 'op' })).toEqual({ user: 'op', salt: '', hash: '' });
|
|
502
|
+
});
|
|
387
503
|
});
|
|
388
504
|
|
|
389
505
|
// ============================================================================
|
|
@@ -516,6 +632,41 @@ describe('ServerConfigSchema — limit knobs', () => {
|
|
|
516
632
|
});
|
|
517
633
|
});
|
|
518
634
|
|
|
635
|
+
// ============================================================================
|
|
636
|
+
// ServerConfigSchema — draft/multiline limits
|
|
637
|
+
// ============================================================================
|
|
638
|
+
|
|
639
|
+
describe('ServerConfigSchema — draft/multiline limits', () => {
|
|
640
|
+
it('defaults multilineMaxBytes to the IRCv3-recommended 4096', () => {
|
|
641
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
642
|
+
expect(cfg.multilineMaxBytes).toBe(4096);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('defaults multilineMaxEntries to 100 (defense-in-depth entry cap)', () => {
|
|
646
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
647
|
+
expect(cfg.multilineMaxEntries).toBe(100);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('accepts a custom multilineMaxEntries', () => {
|
|
651
|
+
const cfg = parseServerConfig({
|
|
652
|
+
serverName: 's',
|
|
653
|
+
networkName: 'n',
|
|
654
|
+
multilineMaxEntries: 250,
|
|
655
|
+
});
|
|
656
|
+
expect(cfg.multilineMaxEntries).toBe(250);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('rejects non-positive multilineMaxEntries', () => {
|
|
660
|
+
expect(() =>
|
|
661
|
+
parseServerConfig({
|
|
662
|
+
serverName: 's',
|
|
663
|
+
networkName: 'n',
|
|
664
|
+
multilineMaxEntries: 0,
|
|
665
|
+
}),
|
|
666
|
+
).toThrowError(/multilineMaxEntries/u);
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
519
670
|
// ============================================================================
|
|
520
671
|
// ServerConfigSchema — failure diagnostics
|
|
521
672
|
// ============================================================================
|
|
@@ -830,6 +981,56 @@ describe('ServerConfigSchema — perIpConnectionRate', () => {
|
|
|
830
981
|
});
|
|
831
982
|
});
|
|
832
983
|
|
|
984
|
+
// ============================================================================
|
|
985
|
+
// ServerConfigSchema — oper (failed-OPER lockout tunables)
|
|
986
|
+
// ============================================================================
|
|
987
|
+
|
|
988
|
+
describe('ServerConfigSchema — oper lockout tunables', () => {
|
|
989
|
+
it('defaults oper.maxFailures to 3 and oper.failureWindowSeconds to 300 when omitted', () => {
|
|
990
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
991
|
+
expect(cfg.oper).toEqual({ maxFailures: 3, failureWindowSeconds: 300 });
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it('accepts explicit positive integers for both fields', () => {
|
|
995
|
+
const cfg = parseServerConfig({
|
|
996
|
+
serverName: 's',
|
|
997
|
+
networkName: 'n',
|
|
998
|
+
oper: { maxFailures: 5, failureWindowSeconds: 60 },
|
|
999
|
+
});
|
|
1000
|
+
expect(cfg.oper).toEqual({ maxFailures: 5, failureWindowSeconds: 60 });
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
it('rejects zero maxFailures', () => {
|
|
1004
|
+
expect(() =>
|
|
1005
|
+
parseServerConfig({
|
|
1006
|
+
serverName: 's',
|
|
1007
|
+
networkName: 'n',
|
|
1008
|
+
oper: { maxFailures: 0, failureWindowSeconds: 300 },
|
|
1009
|
+
}),
|
|
1010
|
+
).toThrowError(/maxFailures/u);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('rejects a non-integer failureWindowSeconds', () => {
|
|
1014
|
+
expect(() =>
|
|
1015
|
+
parseServerConfig({
|
|
1016
|
+
serverName: 's',
|
|
1017
|
+
networkName: 'n',
|
|
1018
|
+
oper: { maxFailures: 3, failureWindowSeconds: 1.5 },
|
|
1019
|
+
}),
|
|
1020
|
+
).toThrowError(/failureWindowSeconds/u);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it('rejects a non-object oper value', () => {
|
|
1024
|
+
expect(() =>
|
|
1025
|
+
parseServerConfig({
|
|
1026
|
+
serverName: 's',
|
|
1027
|
+
networkName: 'n',
|
|
1028
|
+
oper: 'strict',
|
|
1029
|
+
}),
|
|
1030
|
+
).toThrowError(/oper/u);
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
|
|
833
1034
|
// ============================================================================
|
|
834
1035
|
// ServerConfigSchema — sts (Strict Transport Security)
|
|
835
1036
|
// ============================================================================
|
|
@@ -909,6 +1110,56 @@ describe('ServerConfigSchema — sts', () => {
|
|
|
909
1110
|
});
|
|
910
1111
|
});
|
|
911
1112
|
|
|
1113
|
+
// ============================================================================
|
|
1114
|
+
// ServerConfigSchema — chathistory (limit ceiling)
|
|
1115
|
+
// ============================================================================
|
|
1116
|
+
|
|
1117
|
+
describe('ServerConfigSchema — chathistory limit ceiling', () => {
|
|
1118
|
+
it('defaults chathistory.maxLimit to 100 when omitted', () => {
|
|
1119
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
1120
|
+
expect(cfg.chathistory).toEqual({ maxLimit: 100 });
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('accepts an explicit positive integer maxLimit', () => {
|
|
1124
|
+
const cfg = parseServerConfig({
|
|
1125
|
+
serverName: 's',
|
|
1126
|
+
networkName: 'n',
|
|
1127
|
+
chathistory: { maxLimit: 250 },
|
|
1128
|
+
});
|
|
1129
|
+
expect(cfg.chathistory?.maxLimit).toBe(250);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('rejects zero maxLimit', () => {
|
|
1133
|
+
expect(() =>
|
|
1134
|
+
parseServerConfig({
|
|
1135
|
+
serverName: 's',
|
|
1136
|
+
networkName: 'n',
|
|
1137
|
+
chathistory: { maxLimit: 0 },
|
|
1138
|
+
}),
|
|
1139
|
+
).toThrowError(/maxLimit/u);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('rejects a non-integer maxLimit', () => {
|
|
1143
|
+
expect(() =>
|
|
1144
|
+
parseServerConfig({
|
|
1145
|
+
serverName: 's',
|
|
1146
|
+
networkName: 'n',
|
|
1147
|
+
chathistory: { maxLimit: 1.5 },
|
|
1148
|
+
}),
|
|
1149
|
+
).toThrowError(/maxLimit/u);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
it('rejects a non-object chathistory value', () => {
|
|
1153
|
+
expect(() =>
|
|
1154
|
+
parseServerConfig({
|
|
1155
|
+
serverName: 's',
|
|
1156
|
+
networkName: 'n',
|
|
1157
|
+
chathistory: 'unlimited',
|
|
1158
|
+
}),
|
|
1159
|
+
).toThrowError(/chathistory/u);
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
|
|
912
1163
|
// ============================================================================
|
|
913
1164
|
// ServerConfigSchema — hostservAutoApproveVhosts
|
|
914
1165
|
// ============================================================================
|
|
@@ -919,11 +1170,12 @@ describe('ServerConfigSchema — hostservAutoApproveVhosts', () => {
|
|
|
919
1170
|
expect(cfg.hostservAutoApproveVhosts).toBe(false);
|
|
920
1171
|
});
|
|
921
1172
|
|
|
922
|
-
it('accepts an explicit true (auto-approve mode)', () => {
|
|
1173
|
+
it('accepts an explicit true (auto-approve mode) with a non-empty allowlist', () => {
|
|
923
1174
|
const cfg = parseServerConfig({
|
|
924
1175
|
serverName: 's',
|
|
925
1176
|
networkName: 'n',
|
|
926
1177
|
hostservAutoApproveVhosts: true,
|
|
1178
|
+
hostservAutoApproveAllowlist: ['users.example.com'],
|
|
927
1179
|
});
|
|
928
1180
|
expect(cfg.hostservAutoApproveVhosts).toBe(true);
|
|
929
1181
|
});
|
|
@@ -937,6 +1189,73 @@ describe('ServerConfigSchema — hostservAutoApproveVhosts', () => {
|
|
|
937
1189
|
expect(cfg.hostservAutoApproveVhosts).toBe(false);
|
|
938
1190
|
});
|
|
939
1191
|
|
|
1192
|
+
it('refuses to boot auto-approve mode when the allowlist is omitted', () => {
|
|
1193
|
+
expect(() =>
|
|
1194
|
+
parseServerConfig({
|
|
1195
|
+
serverName: 's',
|
|
1196
|
+
networkName: 'n',
|
|
1197
|
+
hostservAutoApproveVhosts: true,
|
|
1198
|
+
}),
|
|
1199
|
+
).toThrowError(/hostservAutoApproveAllowlist/u);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it('refuses to boot auto-approve mode when the allowlist is empty', () => {
|
|
1203
|
+
expect(() =>
|
|
1204
|
+
parseServerConfig({
|
|
1205
|
+
serverName: 's',
|
|
1206
|
+
networkName: 'n',
|
|
1207
|
+
hostservAutoApproveVhosts: true,
|
|
1208
|
+
hostservAutoApproveAllowlist: [],
|
|
1209
|
+
}),
|
|
1210
|
+
).toThrowError(/hostservAutoApproveAllowlist/u);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it('accepts auto-approve mode when the allowlist is non-empty', () => {
|
|
1214
|
+
const cfg = parseServerConfig({
|
|
1215
|
+
serverName: 's',
|
|
1216
|
+
networkName: 'n',
|
|
1217
|
+
hostservAutoApproveVhosts: true,
|
|
1218
|
+
hostservAutoApproveAllowlist: ['users.example.com'],
|
|
1219
|
+
});
|
|
1220
|
+
expect(cfg.hostservAutoApproveVhosts).toBe(true);
|
|
1221
|
+
expect(cfg.hostservAutoApproveAllowlist).toEqual(['users.example.com']);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('defaults the allowlist to empty in queue mode (allowlist unconstrained)', () => {
|
|
1225
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
1226
|
+
expect(cfg.hostservAutoApproveAllowlist).toEqual([]);
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
it('accepts a non-empty allowlist even in queue mode (unused but legal)', () => {
|
|
1230
|
+
const cfg = parseServerConfig({
|
|
1231
|
+
serverName: 's',
|
|
1232
|
+
networkName: 'n',
|
|
1233
|
+
hostservAutoApproveVhosts: false,
|
|
1234
|
+
hostservAutoApproveAllowlist: ['a.example.com', '*.b.example.com'],
|
|
1235
|
+
});
|
|
1236
|
+
expect(cfg.hostservAutoApproveAllowlist).toEqual(['a.example.com', '*.b.example.com']);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it('rejects an allowlist entry that is not a non-empty string', () => {
|
|
1240
|
+
expect(() =>
|
|
1241
|
+
parseServerConfig({
|
|
1242
|
+
serverName: 's',
|
|
1243
|
+
networkName: 'n',
|
|
1244
|
+
hostservAutoApproveAllowlist: [''],
|
|
1245
|
+
}),
|
|
1246
|
+
).toThrowError(/hostservAutoApproveAllowlist/u);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
it('rejects a non-array allowlist', () => {
|
|
1250
|
+
expect(() =>
|
|
1251
|
+
parseServerConfig({
|
|
1252
|
+
serverName: 's',
|
|
1253
|
+
networkName: 'n',
|
|
1254
|
+
hostservAutoApproveAllowlist: 'users.example.com',
|
|
1255
|
+
}),
|
|
1256
|
+
).toThrowError(/hostservAutoApproveAllowlist/u);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
940
1259
|
it('rejects a non-boolean value', () => {
|
|
941
1260
|
expect(() =>
|
|
942
1261
|
parseServerConfig({
|
|
@@ -947,3 +1266,216 @@ describe('ServerConfigSchema — hostservAutoApproveVhosts', () => {
|
|
|
947
1266
|
).toThrowError(/hostservAutoApproveVhosts/u);
|
|
948
1267
|
});
|
|
949
1268
|
});
|
|
1269
|
+
|
|
1270
|
+
// ============================================================================
|
|
1271
|
+
// ServerConfigSchema — sasl (per-connection failure lockout tunables)
|
|
1272
|
+
// ============================================================================
|
|
1273
|
+
|
|
1274
|
+
describe('ServerConfigSchema — sasl', () => {
|
|
1275
|
+
it('defaults sasl to undefined when omitted (reducer falls back to 3 / 60)', () => {
|
|
1276
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
1277
|
+
expect(cfg.sasl).toBeUndefined();
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
it('accepts an explicit sasl config with maxFailures + failureWindowSeconds', () => {
|
|
1281
|
+
const cfg = parseServerConfig({
|
|
1282
|
+
serverName: 's',
|
|
1283
|
+
networkName: 'n',
|
|
1284
|
+
sasl: { maxFailures: 5, failureWindowSeconds: 120 },
|
|
1285
|
+
});
|
|
1286
|
+
expect(cfg.sasl).toEqual({ maxFailures: 5, failureWindowSeconds: 120 });
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
it('accepts a partial sasl config (each knob is independently optional)', () => {
|
|
1290
|
+
const cfg = parseServerConfig({
|
|
1291
|
+
serverName: 's',
|
|
1292
|
+
networkName: 'n',
|
|
1293
|
+
sasl: { maxFailures: 2 },
|
|
1294
|
+
});
|
|
1295
|
+
expect(cfg.sasl).toEqual({ maxFailures: 2 });
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
it('leaves externalEnabled unset when the sasl section is absent (EXTERNAL off)', () => {
|
|
1299
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
1300
|
+
expect(cfg.sasl?.externalEnabled ?? false).toBe(false);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
it('accepts sasl.externalEnabled true (operators opt in to SASL EXTERNAL)', () => {
|
|
1304
|
+
const cfg = parseServerConfig({
|
|
1305
|
+
serverName: 's',
|
|
1306
|
+
networkName: 'n',
|
|
1307
|
+
sasl: { externalEnabled: true },
|
|
1308
|
+
});
|
|
1309
|
+
expect(cfg.sasl?.externalEnabled).toBe(true);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
it('leaves externalEnabled unset within a partial sasl section (other knobs alone do not enable it)', () => {
|
|
1313
|
+
const cfg = parseServerConfig({
|
|
1314
|
+
serverName: 's',
|
|
1315
|
+
networkName: 'n',
|
|
1316
|
+
sasl: { maxFailures: 5, failureWindowSeconds: 120 },
|
|
1317
|
+
});
|
|
1318
|
+
expect(cfg.sasl?.externalEnabled ?? false).toBe(false);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
it('accepts sasl.externalEnabled false (explicit opt-out)', () => {
|
|
1322
|
+
const cfg = parseServerConfig({
|
|
1323
|
+
serverName: 's',
|
|
1324
|
+
networkName: 'n',
|
|
1325
|
+
sasl: { externalEnabled: false },
|
|
1326
|
+
});
|
|
1327
|
+
expect(cfg.sasl?.externalEnabled).toBe(false);
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
it('rejects a non-boolean externalEnabled', () => {
|
|
1331
|
+
expect(() =>
|
|
1332
|
+
parseServerConfig({ serverName: 's', networkName: 'n', sasl: { externalEnabled: 'yes' } }),
|
|
1333
|
+
).toThrowError(/externalEnabled/u);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('accepts sasl.externalUnsupportedMessage (transport-level EXTERNAL non-support)', () => {
|
|
1337
|
+
const cfg = parseServerConfig({
|
|
1338
|
+
serverName: 's',
|
|
1339
|
+
networkName: 'n',
|
|
1340
|
+
sasl: { externalUnsupportedMessage: 'SASL EXTERNAL not supported on this transport' },
|
|
1341
|
+
});
|
|
1342
|
+
expect(cfg.sasl?.externalUnsupportedMessage).toBe(
|
|
1343
|
+
'SASL EXTERNAL not supported on this transport',
|
|
1344
|
+
);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
it('rejects an empty-string externalUnsupportedMessage', () => {
|
|
1348
|
+
expect(() =>
|
|
1349
|
+
parseServerConfig({
|
|
1350
|
+
serverName: 's',
|
|
1351
|
+
networkName: 'n',
|
|
1352
|
+
sasl: { externalUnsupportedMessage: '' },
|
|
1353
|
+
}),
|
|
1354
|
+
).toThrowError(/externalUnsupportedMessage/u);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
it('rejects a non-string externalUnsupportedMessage', () => {
|
|
1358
|
+
expect(() =>
|
|
1359
|
+
parseServerConfig({
|
|
1360
|
+
serverName: 's',
|
|
1361
|
+
networkName: 'n',
|
|
1362
|
+
sasl: { externalUnsupportedMessage: 42 },
|
|
1363
|
+
}),
|
|
1364
|
+
).toThrowError(/externalUnsupportedMessage/u);
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
it('rejects a zero maxFailures', () => {
|
|
1368
|
+
expect(() =>
|
|
1369
|
+
parseServerConfig({ serverName: 's', networkName: 'n', sasl: { maxFailures: 0 } }),
|
|
1370
|
+
).toThrowError(/sasl/u);
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
it('rejects a zero failureWindowSeconds', () => {
|
|
1374
|
+
expect(() =>
|
|
1375
|
+
parseServerConfig({ serverName: 's', networkName: 'n', sasl: { failureWindowSeconds: 0 } }),
|
|
1376
|
+
).toThrowError(/sasl/u);
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
it('rejects a non-number maxFailures', () => {
|
|
1380
|
+
expect(() =>
|
|
1381
|
+
parseServerConfig({ serverName: 's', networkName: 'n', sasl: { maxFailures: '3' } }),
|
|
1382
|
+
).toThrowError(/sasl/u);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
it('rejects a non-object sasl value', () => {
|
|
1386
|
+
expect(() =>
|
|
1387
|
+
parseServerConfig({ serverName: 's', networkName: 'n', sasl: 'PLAIN' }),
|
|
1388
|
+
).toThrowError(/sasl/u);
|
|
1389
|
+
});
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// ============================================================================
|
|
1393
|
+
// ServerConfigSchema — nickServ identify-freeze knobs
|
|
1394
|
+
// ============================================================================
|
|
1395
|
+
|
|
1396
|
+
describe('ServerConfigSchema — nickServ identify freeze', () => {
|
|
1397
|
+
it('leaves the freeze knobs unset when omitted (reducer applies defaults)', () => {
|
|
1398
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
1399
|
+
expect(cfg.nickServ?.maxIdentifyFailures).toBeUndefined();
|
|
1400
|
+
expect(cfg.nickServ?.identifyFreezeSeconds).toBeUndefined();
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
it('accepts explicit maxIdentifyFailures + identifyFreezeSeconds', () => {
|
|
1404
|
+
const cfg = parseServerConfig({
|
|
1405
|
+
serverName: 's',
|
|
1406
|
+
networkName: 'n',
|
|
1407
|
+
nickServ: { maxIdentifyFailures: 3, identifyFreezeSeconds: 120 },
|
|
1408
|
+
});
|
|
1409
|
+
expect(cfg.nickServ?.maxIdentifyFailures).toBe(3);
|
|
1410
|
+
expect(cfg.nickServ?.identifyFreezeSeconds).toBe(120);
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it('rejects a non-positive maxIdentifyFailures', () => {
|
|
1414
|
+
expect(() =>
|
|
1415
|
+
parseServerConfig({
|
|
1416
|
+
serverName: 's',
|
|
1417
|
+
networkName: 'n',
|
|
1418
|
+
nickServ: { maxIdentifyFailures: 0 },
|
|
1419
|
+
}),
|
|
1420
|
+
).toThrowError(/maxIdentifyFailures/u);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
it('rejects a non-integer identifyFreezeSeconds', () => {
|
|
1424
|
+
expect(() =>
|
|
1425
|
+
parseServerConfig({
|
|
1426
|
+
serverName: 's',
|
|
1427
|
+
networkName: 'n',
|
|
1428
|
+
nickServ: { identifyFreezeSeconds: 1.5 },
|
|
1429
|
+
}),
|
|
1430
|
+
).toThrowError(/identifyFreezeSeconds/u);
|
|
1431
|
+
});
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
// ============================================================================
|
|
1435
|
+
// ServerConfigSchema — adapter inbound frame-rate limit
|
|
1436
|
+
// ============================================================================
|
|
1437
|
+
|
|
1438
|
+
describe('ServerConfigSchema — adapter frame-rate limit', () => {
|
|
1439
|
+
it('defaults the adapter frame window to 50 frames / 5 seconds', () => {
|
|
1440
|
+
const cfg = parseServerConfig({ serverName: 's', networkName: 'n' });
|
|
1441
|
+
expect(cfg.adapter).toEqual({ maxFramesPerWindow: 50, frameWindowSeconds: 5 });
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('accepts explicit adapter frame-window overrides', () => {
|
|
1445
|
+
const cfg = parseServerConfig({
|
|
1446
|
+
serverName: 's',
|
|
1447
|
+
networkName: 'n',
|
|
1448
|
+
adapter: { maxFramesPerWindow: 10, frameWindowSeconds: 2 },
|
|
1449
|
+
});
|
|
1450
|
+
expect(cfg.adapter).toEqual({ maxFramesPerWindow: 10, frameWindowSeconds: 2 });
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('fills a partially-supplied adapter section with per-field defaults', () => {
|
|
1454
|
+
const cfg = parseServerConfig({
|
|
1455
|
+
serverName: 's',
|
|
1456
|
+
networkName: 'n',
|
|
1457
|
+
adapter: { frameWindowSeconds: 30 },
|
|
1458
|
+
});
|
|
1459
|
+
expect(cfg.adapter).toEqual({ maxFramesPerWindow: 50, frameWindowSeconds: 30 });
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
it('rejects a non-positive maxFramesPerWindow', () => {
|
|
1463
|
+
expect(() =>
|
|
1464
|
+
parseServerConfig({
|
|
1465
|
+
serverName: 's',
|
|
1466
|
+
networkName: 'n',
|
|
1467
|
+
adapter: { maxFramesPerWindow: 0 },
|
|
1468
|
+
}),
|
|
1469
|
+
).toThrowError(/maxFramesPerWindow/u);
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
it('rejects a non-integer frameWindowSeconds', () => {
|
|
1473
|
+
expect(() =>
|
|
1474
|
+
parseServerConfig({
|
|
1475
|
+
serverName: 's',
|
|
1476
|
+
networkName: 'n',
|
|
1477
|
+
adapter: { frameWindowSeconds: 1.5 },
|
|
1478
|
+
}),
|
|
1479
|
+
).toThrowError(/frameWindowSeconds/u);
|
|
1480
|
+
});
|
|
1481
|
+
});
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
type HashedAccountCredential,
|
|
4
|
-
HashedAccountStore,
|
|
5
4
|
hashAccountCredential,
|
|
6
5
|
verifyHashedPassword,
|
|
7
6
|
} from '../src/credential-hashing';
|
|
8
|
-
import type { AccountStore, SaslPayload } from '../src/ports';
|
|
9
7
|
|
|
10
8
|
describe('hashAccountCredential', () => {
|
|
11
9
|
it('produces an entry keyed by the username with scrypt', () => {
|
|
@@ -92,79 +90,3 @@ describe('verifyHashedPassword', () => {
|
|
|
92
90
|
expect(verifyHashedPassword('s3cret', entry)).toBe(false);
|
|
93
91
|
});
|
|
94
92
|
});
|
|
95
|
-
|
|
96
|
-
describe('HashedAccountStore', () => {
|
|
97
|
-
function plain(username: string, password: string): SaslPayload {
|
|
98
|
-
return { kind: 'PLAIN', username, password };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
it('implements the AccountStore port', () => {
|
|
102
|
-
const store: AccountStore = new HashedAccountStore([hashAccountCredential('alice', 's3cret')]);
|
|
103
|
-
expect(store).toBeDefined();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('accepts valid PLAIN credentials and returns the account name', () => {
|
|
107
|
-
const store = new HashedAccountStore([hashAccountCredential('alice', 's3cret')]);
|
|
108
|
-
expect(store.verify('PLAIN', plain('alice', 's3cret'))).toEqual({
|
|
109
|
-
ok: true,
|
|
110
|
-
account: 'alice',
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('rejects a wrong password', () => {
|
|
115
|
-
const store = new HashedAccountStore([hashAccountCredential('alice', 's3cret')]);
|
|
116
|
-
expect(store.verify('PLAIN', plain('alice', 'wrong')).ok).toBe(false);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('rejects an unknown username', () => {
|
|
120
|
-
const store = new HashedAccountStore([hashAccountCredential('alice', 's3cret')]);
|
|
121
|
-
expect(store.verify('PLAIN', plain('bob', 's3cret')).ok).toBe(false);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('rejects a non-PLAIN mechanism', () => {
|
|
125
|
-
const store = new HashedAccountStore([hashAccountCredential('alice', 's3cret')]);
|
|
126
|
-
expect(store.verify('EXTERNAL', plain('alice', 's3cret')).ok).toBe(false);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('rejects a RAW payload', () => {
|
|
130
|
-
const store = new HashedAccountStore([hashAccountCredential('alice', 's3cret')]);
|
|
131
|
-
expect(store.verify('PLAIN', { kind: 'RAW', data: 'whatever' }).ok).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('handles multiple loaded accounts', () => {
|
|
135
|
-
const store = new HashedAccountStore([
|
|
136
|
-
hashAccountCredential('alice', 'a-secret'),
|
|
137
|
-
hashAccountCredential('bob', 'b-secret'),
|
|
138
|
-
]);
|
|
139
|
-
expect(store.verify('PLAIN', plain('alice', 'a-secret'))).toEqual({
|
|
140
|
-
ok: true,
|
|
141
|
-
account: 'alice',
|
|
142
|
-
});
|
|
143
|
-
expect(store.verify('PLAIN', plain('bob', 'b-secret'))).toEqual({
|
|
144
|
-
ok: true,
|
|
145
|
-
account: 'bob',
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('fails every verify when constructed with no entries', () => {
|
|
150
|
-
const store = new HashedAccountStore([]);
|
|
151
|
-
expect(store.verify('PLAIN', plain('alice', 's3cret')).ok).toBe(false);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('reports the number of loaded entries via size', () => {
|
|
155
|
-
const store = new HashedAccountStore([
|
|
156
|
-
hashAccountCredential('alice', 'a'),
|
|
157
|
-
hashAccountCredential('bob', 'b'),
|
|
158
|
-
]);
|
|
159
|
-
expect(store.size).toBe(2);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('round-trips an externally-constructed HashedAccountCredential', () => {
|
|
163
|
-
const row: HashedAccountCredential = hashAccountCredential('carol', 'p@ss');
|
|
164
|
-
const store = new HashedAccountStore([row]);
|
|
165
|
-
expect(store.verify('PLAIN', plain('carol', 'p@ss'))).toEqual({
|
|
166
|
-
ok: true,
|
|
167
|
-
account: 'carol',
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
});
|