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
|
@@ -18,24 +18,125 @@
|
|
|
18
18
|
|
|
19
19
|
import { z } from 'zod';
|
|
20
20
|
import { BUILD_DATE, PACKAGE_VERSION } from './build-info.js';
|
|
21
|
-
import { DEFAULT_MULTILINE_MAX_BYTES } from './caps/capabilities.js';
|
|
21
|
+
import { DEFAULT_MULTILINE_MAX_BYTES, DEFAULT_MULTILINE_MAX_ENTRIES } from './caps/capabilities.js';
|
|
22
|
+
import type { OperCred } from './types.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Narrows the schema's loose `{user, password?, salt?, hash?}` parse
|
|
26
|
+
* output into the discriminated {@link OperCred} union so the parsed
|
|
27
|
+
* config is directly assignable to `ServerConfig.operCreds`.
|
|
28
|
+
*
|
|
29
|
+
* When `password` is present the entry narrows to the legacy plaintext
|
|
30
|
+
* shape; otherwise the entry narrows to the hashed shape. The
|
|
31
|
+
* {@link OperCredSchema} `superRefine` guarantees exactly one of the two
|
|
32
|
+
* arms holds for any input that reaches parse success; the `?? ''`
|
|
33
|
+
* fallbacks below are defensive only (unreachable through
|
|
34
|
+
* `parseServerConfig`) and exist so the helper is total.
|
|
35
|
+
*/
|
|
36
|
+
export function narrowOperCred(data: {
|
|
37
|
+
user: string;
|
|
38
|
+
password?: string | undefined;
|
|
39
|
+
salt?: string | undefined;
|
|
40
|
+
hash?: string | undefined;
|
|
41
|
+
}): OperCred {
|
|
42
|
+
if (data.password !== undefined) {
|
|
43
|
+
return { user: data.user, password: data.password };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
user: data.user,
|
|
47
|
+
salt: data.salt ?? '',
|
|
48
|
+
hash: data.hash ?? '',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
22
51
|
|
|
23
52
|
/**
|
|
24
53
|
* Schema for a single IRC operator credential.
|
|
25
54
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
55
|
+
* Accepts EITHER the at-rest hashed form (`{ user, salt, hash }`,
|
|
56
|
+
* recommended) OR the legacy plaintext form (`{ user, password }`,
|
|
57
|
+
* deprecated). The two shapes are mutually exclusive: a credential that
|
|
58
|
+
* supplies both `password` and `{salt, hash}` is rejected so operators
|
|
59
|
+
* cannot accidentally configure an ambiguous credential. The OPER auth
|
|
60
|
+
* path consults whichever shape is present.
|
|
61
|
+
*
|
|
62
|
+
* The plaintext form is DEPRECATED: retained for a single
|
|
63
|
+
* deprecation-cycle window so deployments can rotate to the hashed form
|
|
64
|
+
* without downtime. Adapters and `OPER` authentication still verify it,
|
|
65
|
+
* but emit a one-shot `warn` per boot when it is encountered. Migrate
|
|
66
|
+
* via `tools/hash-oper-cred.ts`; the plaintext form will be removed in a
|
|
67
|
+
* future release.
|
|
68
|
+
*
|
|
69
|
+
* Implemented as a unified object schema with `superRefine` rather than
|
|
70
|
+
* `z.union` so the rejection error messages name the offending field
|
|
71
|
+
* (Zod unions emit a generic "Invalid input" that hides which half is
|
|
72
|
+
* malformed). The trailing {@link narrowOperCred} transform narrows the
|
|
73
|
+
* loose parse output back into the {@link OperCred} union so
|
|
74
|
+
* `ParsedServerConfig.operCreds` is assignable to `ServerConfig`.
|
|
28
75
|
*/
|
|
29
|
-
export const OperCredSchema = z
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
76
|
+
export const OperCredSchema = z
|
|
77
|
+
.object({
|
|
78
|
+
user: z.string().min(1),
|
|
79
|
+
password: z.string().min(1).optional(),
|
|
80
|
+
salt: z.string().min(1).optional(),
|
|
81
|
+
hash: z.string().min(1).optional(),
|
|
82
|
+
})
|
|
83
|
+
.superRefine((data, ctx) => {
|
|
84
|
+
const hasPassword = data.password !== undefined;
|
|
85
|
+
const hasSalt = data.salt !== undefined;
|
|
86
|
+
const hasHash = data.hash !== undefined;
|
|
87
|
+
|
|
88
|
+
// The plaintext form requires `password` and forbids `salt`/`hash`.
|
|
89
|
+
// The hashed form requires BOTH `salt` and `hash` and forbids
|
|
90
|
+
// `password`. Anything else is ambiguous and rejected with a
|
|
91
|
+
// field-named error so operators can fix the config without
|
|
92
|
+
// round-tripping through Zod's error tree.
|
|
93
|
+
if (hasPassword && (hasSalt || hasHash)) {
|
|
94
|
+
ctx.addIssue({
|
|
95
|
+
code: z.ZodIssueCode.custom,
|
|
96
|
+
message:
|
|
97
|
+
'Oper cred must be either {user,password} (legacy plaintext) or {user,salt,hash} (hashed), not a mix',
|
|
98
|
+
path: ['password'],
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!hasPassword && !(hasSalt && hasHash)) {
|
|
103
|
+
// Either salt or hash (or both) is missing on the hashed arm.
|
|
104
|
+
if (!hasSalt) {
|
|
105
|
+
ctx.addIssue({
|
|
106
|
+
code: z.ZodIssueCode.custom,
|
|
107
|
+
message:
|
|
108
|
+
'Required for the hashed form (or supply `password` for the legacy plaintext form)',
|
|
109
|
+
path: ['salt'],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (!hasHash) {
|
|
113
|
+
ctx.addIssue({
|
|
114
|
+
code: z.ZodIssueCode.custom,
|
|
115
|
+
message:
|
|
116
|
+
'Required for the hashed form (or supply `password` for the legacy plaintext form)',
|
|
117
|
+
path: ['hash'],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (!hasSalt && !hasHash) {
|
|
121
|
+
// Neither salt nor hash AND no password: the cred is missing
|
|
122
|
+
// every authentication field. Surface `password` so operators
|
|
123
|
+
// coming from the legacy schema see the familiar error.
|
|
124
|
+
ctx.addIssue({
|
|
125
|
+
code: z.ZodIssueCode.custom,
|
|
126
|
+
message:
|
|
127
|
+
'Required for the legacy plaintext form (or supply `salt` + `hash` for the hashed form)',
|
|
128
|
+
path: ['password'],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
.transform(narrowOperCred);
|
|
33
134
|
|
|
34
135
|
/**
|
|
35
136
|
* Schema for a single SASL PLAIN account credential. The `username` is also
|
|
36
137
|
* the canonical account name recorded on the connection (for `extended-join`
|
|
37
|
-
* / `account-tag`). Both fields are non-empty strings; adapters seed
|
|
38
|
-
*
|
|
138
|
+
* / `account-tag`). Both fields are non-empty strings; adapters seed the
|
|
139
|
+
* `InMemoryServicesStore` from this list at boot.
|
|
39
140
|
*/
|
|
40
141
|
export const SaslAccountSchema = z.object({
|
|
41
142
|
username: z.string().min(1),
|
|
@@ -142,6 +243,22 @@ export const DEFAULT_FLOOD_CONTROL_CONFIG: {
|
|
|
142
243
|
disconnectThreshold: 0,
|
|
143
244
|
};
|
|
144
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Default number of consecutive failed `OPER` attempts (from one source
|
|
248
|
+
* host, within {@link DEFAULT_OPER_FAILURE_WINDOW_SECONDS}) before the
|
|
249
|
+
* per-IP lockout gate starts rejecting `OPER` with `491 ERR_NOOPERHOST`
|
|
250
|
+
* without consulting the credential store. Overridable per deployment via
|
|
251
|
+
* `ServerConfig.oper.maxFailures`.
|
|
252
|
+
*/
|
|
253
|
+
export const DEFAULT_OPER_MAX_FAILURES = 3;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Default sliding window (seconds) for the per-IP failed-`OPER` counter.
|
|
257
|
+
* Failures older than the window no longer count towards the lockout.
|
|
258
|
+
* Overridable per deployment via `ServerConfig.oper.failureWindowSeconds`.
|
|
259
|
+
*/
|
|
260
|
+
export const DEFAULT_OPER_FAILURE_WINDOW_SECONDS = 300;
|
|
261
|
+
|
|
145
262
|
/**
|
|
146
263
|
* Default server version surfaced in `002`/`004`/`351`/`371` when a deployment
|
|
147
264
|
* does not supply one. Sourced from `irc-core/package.json` at build time via
|
|
@@ -173,12 +290,139 @@ export const DEFAULT_CREATED_TEXT: string = BUILD_DATE;
|
|
|
173
290
|
*/
|
|
174
291
|
export const DEFAULT_NICK_ENFORCE_GRACE_MS = 30_000;
|
|
175
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Default minimum password length enforced by NickServ `SET PASSWORD`.
|
|
295
|
+
* Mirrors common IRC services (Atheme/Anope) and modern guidance (NIST
|
|
296
|
+
* SP 800-63B). Overridable per deployment via
|
|
297
|
+
* `ServerConfig.nickServ.minPasswordLength`. A new password shorter than
|
|
298
|
+
* the floor is rejected before hashing.
|
|
299
|
+
*/
|
|
300
|
+
export const DEFAULT_MIN_PASSWORD_LENGTH = 8;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Default number of failed `AUTHENTICATE` attempts (per connection, within
|
|
304
|
+
* the sliding failure window) before the SASL reducer disconnects the
|
|
305
|
+
* connection. Overridable per deployment via
|
|
306
|
+
* `ServerConfig.sasl.maxFailures`.
|
|
307
|
+
*/
|
|
308
|
+
export const DEFAULT_SASL_MAX_FAILURES = 3;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Default sliding-window length (seconds) the SASL failure tracker counts
|
|
312
|
+
* within; older failures no longer count toward the lockout. Overridable per
|
|
313
|
+
* deployment via `ServerConfig.sasl.failureWindowSeconds`.
|
|
314
|
+
*/
|
|
315
|
+
export const DEFAULT_SASL_FAILURE_WINDOW_SECONDS = 60;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Default failed-IDENTIFY threshold that freezes a NickServ account.
|
|
319
|
+
* After this many failed `IDENTIFY` attempts against one registered nick
|
|
320
|
+
* inside the freeze window, further attempts are rejected with a freeze
|
|
321
|
+
* notice BEFORE the scrypt verification runs. Overridable per deployment
|
|
322
|
+
* via `ServerConfig.nickServ.maxIdentifyFailures`.
|
|
323
|
+
*/
|
|
324
|
+
export const DEFAULT_MAX_IDENTIFY_FAILURES = 5;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Default identify-freeze window (seconds). Failed `IDENTIFY` attempts
|
|
328
|
+
* older than this window stop counting towards the freeze (sliding
|
|
329
|
+
* window), so the freeze lifts once the failures age out. Overridable per
|
|
330
|
+
* deployment via `ServerConfig.nickServ.identifyFreezeSeconds`.
|
|
331
|
+
*/
|
|
332
|
+
export const DEFAULT_IDENTIFY_FREEZE_SECONDS = 300;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Default ceiling for the per-connection inbound frame window enforced
|
|
336
|
+
* at the adapter boundary (see `frame-rate-limit.ts`). Overridable per
|
|
337
|
+
* deployment via `ServerConfig.adapter.maxFramesPerWindow`.
|
|
338
|
+
*/
|
|
339
|
+
export const DEFAULT_MAX_FRAMES_PER_WINDOW = 50;
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Default sliding-window length (seconds) for the per-connection inbound
|
|
343
|
+
* frame limit at the adapter boundary. Frames older than the window stop
|
|
344
|
+
* counting, so a connection that pauses longer than the window starts
|
|
345
|
+
* fresh. Overridable per deployment via
|
|
346
|
+
* `ServerConfig.adapter.frameWindowSeconds`.
|
|
347
|
+
*/
|
|
348
|
+
export const DEFAULT_FRAME_WINDOW_SECONDS = 5;
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Default ceiling for a client-supplied `CHATHISTORY` limit. A request
|
|
352
|
+
* like `CHATHISTORY LATEST #chan * 999999999` is silently capped at this
|
|
353
|
+
* many messages so a future higher-capacity `MessageStore` cannot be
|
|
354
|
+
* tricked into materializing a huge result set. Overridable per
|
|
355
|
+
* deployment via `ServerConfig.chathistory.maxLimit`.
|
|
356
|
+
*/
|
|
357
|
+
export const DEFAULT_MAX_CHATHISTORY_LIMIT = 100;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* `draft/chathistory` tunables. The client-supplied query limit is
|
|
361
|
+
* silently capped at `maxLimit`; the cap guards the `MessageStore.query`
|
|
362
|
+
* seam against oversized materialization regardless of the store's
|
|
363
|
+
* backing capacity.
|
|
364
|
+
*/
|
|
365
|
+
export const ChathistoryConfigSchema = z.object({
|
|
366
|
+
/**
|
|
367
|
+
* Maximum number of messages one `CHATHISTORY` query may return.
|
|
368
|
+
* Defaults to {@link DEFAULT_MAX_CHATHISTORY_LIMIT} (100).
|
|
369
|
+
*/
|
|
370
|
+
maxLimit: z.number().int().positive().default(DEFAULT_MAX_CHATHISTORY_LIMIT),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Adapter-boundary tunables. These knobs are consumed by the adapters'
|
|
375
|
+
* inbound-frame gates (the CF Durable Object's `webSocketMessage` and the
|
|
376
|
+
* AWS `$default` handler) — NOT by the reducers — so they live in their
|
|
377
|
+
* own section rather than on the reducer-facing limit list. The
|
|
378
|
+
* per-connection frame window fires BEFORE the actor / storage write,
|
|
379
|
+
* compounding the in-actor flood control with a hard cost ceiling.
|
|
380
|
+
*/
|
|
381
|
+
export const AdapterConfigSchema = z.object({
|
|
382
|
+
/**
|
|
383
|
+
* Maximum inbound frames one connection may deliver inside the sliding
|
|
384
|
+
* window before the adapter closes it with RFC 6455 close code 1008
|
|
385
|
+
* after an `ERROR :Closing link (flooding)` line. Defaults to
|
|
386
|
+
* {@link DEFAULT_MAX_FRAMES_PER_WINDOW} (50).
|
|
387
|
+
*/
|
|
388
|
+
maxFramesPerWindow: z.number().int().positive().default(DEFAULT_MAX_FRAMES_PER_WINDOW),
|
|
389
|
+
/**
|
|
390
|
+
* Sliding-window length in seconds. Defaults to
|
|
391
|
+
* {@link DEFAULT_FRAME_WINDOW_SECONDS} (5).
|
|
392
|
+
*/
|
|
393
|
+
frameWindowSeconds: z.number().int().positive().default(DEFAULT_FRAME_WINDOW_SECONDS),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Per-IP failed-`OPER` lockout tunables. After `maxFailures` credential
|
|
398
|
+
* failures from one source host within `failureWindowSeconds`, further
|
|
399
|
+
* `OPER` attempts from that host are rejected with `491 ERR_NOOPERHOST`
|
|
400
|
+
* before the credential store is consulted (online brute-force throttle).
|
|
401
|
+
*/
|
|
402
|
+
export const OperThrottleConfigSchema = z.object({
|
|
403
|
+
/**
|
|
404
|
+
* Consecutive failed `OPER` attempts (per host, per window) that trip
|
|
405
|
+
* the lockout. Defaults to {@link DEFAULT_OPER_MAX_FAILURES} (3).
|
|
406
|
+
*/
|
|
407
|
+
maxFailures: z.number().int().positive().default(DEFAULT_OPER_MAX_FAILURES),
|
|
408
|
+
/**
|
|
409
|
+
* Sliding window length in seconds. Failures older than the window no
|
|
410
|
+
* longer count. Defaults to {@link DEFAULT_OPER_FAILURE_WINDOW_SECONDS}
|
|
411
|
+
* (300).
|
|
412
|
+
*/
|
|
413
|
+
failureWindowSeconds: z.number().int().positive().default(DEFAULT_OPER_FAILURE_WINDOW_SECONDS),
|
|
414
|
+
});
|
|
415
|
+
|
|
176
416
|
/**
|
|
177
417
|
* The full server-config schema. Optional fields default to deployment-
|
|
178
418
|
* sensible values so a minimal `{ serverName, networkName }` config is
|
|
179
419
|
* enough to boot; production deployments override the defaults.
|
|
420
|
+
*
|
|
421
|
+
* Composed as a plain object schema that the exported
|
|
422
|
+
* {@link ServerConfigSchema} wraps with cross-field refinements (the
|
|
423
|
+
* HostServ auto-approve allowlist boot guard).
|
|
180
424
|
*/
|
|
181
|
-
|
|
425
|
+
const ServerConfigSchemaObject = z.object({
|
|
182
426
|
/** Server hostname shown in numerics (`:serverName 001 nick ...`). */
|
|
183
427
|
serverName: z.string().min(1),
|
|
184
428
|
/** Network name advertised in `005 NETWORK=...` and WHOIS replies. */
|
|
@@ -271,13 +515,21 @@ export const ServerConfigSchema = z.object({
|
|
|
271
515
|
operCreds: z.array(OperCredSchema).default([]),
|
|
272
516
|
|
|
273
517
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
518
|
+
* Per-IP failed-`OPER` lockout tunables (online brute-force throttle).
|
|
519
|
+
* Omit entirely to inherit the documented defaults (3 failures / 300s).
|
|
520
|
+
*/
|
|
521
|
+
oper: OperThrottleConfigSchema.default({
|
|
522
|
+
maxFailures: DEFAULT_OPER_MAX_FAILURES,
|
|
523
|
+
failureWindowSeconds: DEFAULT_OPER_FAILURE_WINDOW_SECONDS,
|
|
524
|
+
}),
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* SASL PLAIN seed credentials. The adapter ingests these into the
|
|
528
|
+
* `ServicesStore` at boot via `registerNick` (idempotent — existing
|
|
529
|
+
* registrations win). `AUTHENTICATE PLAIN`, `PASS <nick>:<password>`,
|
|
530
|
+
* and NickServ `IDENTIFY` all verify against the resulting
|
|
531
|
+
* `nickserv_accounts` rows. Empty array (the default) leaves the seed
|
|
532
|
+
* empty; accounts can still be created at runtime via NickServ REGISTER.
|
|
281
533
|
*/
|
|
282
534
|
saslAccounts: z.array(SaslAccountSchema).default([]),
|
|
283
535
|
|
|
@@ -328,6 +580,14 @@ export const ServerConfigSchema = z.object({
|
|
|
328
580
|
*/
|
|
329
581
|
multilineMaxBytes: z.number().int().positive().default(DEFAULT_MULTILINE_MAX_BYTES),
|
|
330
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Per-batch entry ceiling for IRCv3 `draft/multiline`, enforced
|
|
585
|
+
* incrementally on each accumulated inner line (defense-in-depth against
|
|
586
|
+
* a client that streams many tiny lines and never closes the batch).
|
|
587
|
+
* Defaults to {@link DEFAULT_MULTILINE_MAX_ENTRIES} (100).
|
|
588
|
+
*/
|
|
589
|
+
multilineMaxEntries: z.number().int().positive().default(DEFAULT_MULTILINE_MAX_ENTRIES),
|
|
590
|
+
|
|
331
591
|
/**
|
|
332
592
|
* Per-connection MONITOR watchlist ceiling, advertised as `MONITOR=<n>`
|
|
333
593
|
* in `005 RPL_ISUPPORT`. Omit to inherit the monitor reducer's built-in
|
|
@@ -358,16 +618,155 @@ export const ServerConfigSchema = z.object({
|
|
|
358
618
|
nickEnforceGraceMs: z.number().int().nonnegative().optional(),
|
|
359
619
|
|
|
360
620
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
621
|
+
* NickServ-specific tunables. The minimum length enforced by
|
|
622
|
+
* `SET PASSWORD` and the IDENTIFY freeze thresholds are configurable;
|
|
623
|
+
* the password maximum (256) is fixed to bound the scrypt input
|
|
624
|
+
* budget. Omit entirely to inherit the documented defaults.
|
|
625
|
+
*/
|
|
626
|
+
nickServ: z
|
|
627
|
+
.object({
|
|
628
|
+
/**
|
|
629
|
+
* Minimum length a new password must meet to be accepted by
|
|
630
|
+
* `SET PASSWORD`. Defaults to {@link DEFAULT_MIN_PASSWORD_LENGTH}
|
|
631
|
+
* (8) when unset. A shorter new password is rejected before hashing
|
|
632
|
+
* with `Password is too short (minimum <n> characters).`.
|
|
633
|
+
*/
|
|
634
|
+
minPasswordLength: z.number().int().positive().optional(),
|
|
635
|
+
/**
|
|
636
|
+
* Failed-IDENTIFY threshold that freezes the account. After this
|
|
637
|
+
* many failed `IDENTIFY` attempts against one registered nick
|
|
638
|
+
* within {@link identifyFreezeSeconds}, further attempts are
|
|
639
|
+
* rejected with a freeze notice before scrypt runs. Defaults to
|
|
640
|
+
* {@link DEFAULT_MAX_IDENTIFY_FAILURES} (5) when unset.
|
|
641
|
+
*/
|
|
642
|
+
maxIdentifyFailures: z.number().int().positive().optional(),
|
|
643
|
+
/**
|
|
644
|
+
* Sliding window (seconds) the failed-IDENTIFY threshold counts
|
|
645
|
+
* within. Failures older than the window stop counting, so the
|
|
646
|
+
* freeze lifts once they age out. Defaults to
|
|
647
|
+
* {@link DEFAULT_IDENTIFY_FREEZE_SECONDS} (300) when unset.
|
|
648
|
+
*/
|
|
649
|
+
identifyFreezeSeconds: z.number().int().positive().optional(),
|
|
650
|
+
})
|
|
651
|
+
.optional(),
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* SASL brute-force hardening tunables. The SASL reducer disconnects a
|
|
655
|
+
* connection after `maxFailures` failed `AUTHENTICATE` attempts within
|
|
656
|
+
* `failureWindowSeconds` (a sliding window; a successful login resets
|
|
657
|
+
* the count). Omit entirely (or omit either knob) to inherit the
|
|
658
|
+
* documented defaults (3 failures / 60 s).
|
|
659
|
+
*/
|
|
660
|
+
sasl: z
|
|
661
|
+
.object({
|
|
662
|
+
/**
|
|
663
|
+
* Failed `AUTHENTICATE` attempts (per connection, within the
|
|
664
|
+
* window) before the connection is disconnected with
|
|
665
|
+
* `ERROR :Closing link: too many failed SASL attempts`. Defaults to
|
|
666
|
+
* {@link DEFAULT_SASL_MAX_FAILURES} (3) when unset.
|
|
667
|
+
*/
|
|
668
|
+
maxFailures: z.number().int().positive().optional(),
|
|
669
|
+
/**
|
|
670
|
+
* Sliding-window length (seconds) the failure tracker counts
|
|
671
|
+
* within. Failures older than the window no longer count. Defaults
|
|
672
|
+
* to {@link DEFAULT_SASL_FAILURE_WINDOW_SECONDS} (60) when unset.
|
|
673
|
+
*/
|
|
674
|
+
failureWindowSeconds: z.number().int().positive().optional(),
|
|
675
|
+
/**
|
|
676
|
+
* Operator opt-in for the certificate-backed SASL EXTERNAL
|
|
677
|
+
* mechanism. Unset (the default) keeps EXTERNAL off: the mechanism
|
|
678
|
+
* is neither advertised in the `sasl` cap / `908 ERR_SASLMECHS`
|
|
679
|
+
* list nor accepted by `AUTHENTICATE EXTERNAL`, so deployments
|
|
680
|
+
* that have not wired edge mTLS (CF API Shield / AWS APIGW
|
|
681
|
+
* client certs) never see half-configured exchanges. Enabling
|
|
682
|
+
* requires all three of: this flag, a bound mTLS identity source,
|
|
683
|
+
* and a secure (TLS) connection — see the SASL reducer.
|
|
684
|
+
*
|
|
685
|
+
* The adapters surface this as the `EXTERNAL_ENABLED` env var
|
|
686
|
+
* ('true'/'1' to enable). Accounts opt into EXTERNAL per-binding
|
|
687
|
+
* (cert fingerprint / subject DN on the NickServ record), not via
|
|
688
|
+
* this flag.
|
|
689
|
+
*/
|
|
690
|
+
externalEnabled: z.boolean().optional(),
|
|
691
|
+
/**
|
|
692
|
+
* Transport-level EXTERNAL non-support override (see
|
|
693
|
+
* `SaslConfig.externalUnsupportedMessage`): when set, the
|
|
694
|
+
* deployment's transport cannot surface a client-cert identity,
|
|
695
|
+
* so `AUTHENTICATE EXTERNAL` is rejected up front with a
|
|
696
|
+
* `904 ERR_SASLFAIL` carrying this message and the mechanism is
|
|
697
|
+
* never advertised. Takes precedence over `externalEnabled`.
|
|
698
|
+
*/
|
|
699
|
+
externalUnsupportedMessage: z.string().min(1).optional(),
|
|
700
|
+
})
|
|
701
|
+
.optional(),
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Adapter-boundary tunables (inbound frame-rate window). Omit entirely
|
|
705
|
+
* to inherit the documented defaults (50 frames / 5 s).
|
|
706
|
+
*/
|
|
707
|
+
adapter: AdapterConfigSchema.default({
|
|
708
|
+
maxFramesPerWindow: DEFAULT_MAX_FRAMES_PER_WINDOW,
|
|
709
|
+
frameWindowSeconds: DEFAULT_FRAME_WINDOW_SECONDS,
|
|
710
|
+
}),
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* `draft/chathistory` tunables (client-supplied limit ceiling). Omit
|
|
714
|
+
* entirely to inherit the documented default (100).
|
|
715
|
+
*/
|
|
716
|
+
chathistory: ChathistoryConfigSchema.default({
|
|
717
|
+
maxLimit: DEFAULT_MAX_CHATHISTORY_LIMIT,
|
|
718
|
+
}),
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* HostServ approval mode for `REQUEST <vhost>`. `false` (the default) is
|
|
722
|
+
* oper-queue mode: `REQUEST` records a pending request that an oper must
|
|
723
|
+
* `APPROVE` (or `REJECT`) via HostServ — this is the recommended (and
|
|
724
|
+
* long-term only) mode. `true` preserves the legacy auto-approve
|
|
725
|
+
* behaviour — the requested vhost is recorded against the account
|
|
726
|
+
* immediately (no oper review).
|
|
727
|
+
*
|
|
728
|
+
* Auto-approve is hard-constrained for security: a non-empty
|
|
729
|
+
* {@link hostservAutoApproveAllowlist} MUST accompany `true` or config
|
|
730
|
+
* parsing fails at boot, and every auto-approved vhost must still clear
|
|
731
|
+
* the built-in denylist (reserved labels + this server's own hostname).
|
|
732
|
+
*
|
|
733
|
+
* @deprecated Auto-approve is deprecated. Queue mode (`false`, the
|
|
734
|
+
* default) is the long-term path; a future release will remove the
|
|
735
|
+
* auto-approve arm entirely. Migrate deployments to oper review.
|
|
369
736
|
*/
|
|
370
737
|
hostservAutoApproveVhosts: z.boolean().default(false),
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Explicit allowlist constraining which vhosts HostServ may auto-approve
|
|
741
|
+
* when {@link hostservAutoApproveVhosts} is `true`. An entry is either an
|
|
742
|
+
* exact hostname (`users.example.com`) or a wildcard suffix
|
|
743
|
+
* (`*.users.example.com`, matching any depth beneath it but not the bare
|
|
744
|
+
* suffix). Matching is case-insensitive.
|
|
745
|
+
*
|
|
746
|
+
* Ignored in queue mode (opers vet requests themselves). Required and
|
|
747
|
+
* non-empty when auto-approve is enabled — parsing fails at boot
|
|
748
|
+
* otherwise, because an unconstrained allowlist would let any identified
|
|
749
|
+
* user self-assign arbitrary hostnames.
|
|
750
|
+
*/
|
|
751
|
+
hostservAutoApproveAllowlist: z.array(z.string().min(1)).default([]),
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Cross-field boot guard: auto-approve mode must declare a non-empty
|
|
756
|
+
* vhost allowlist. Applied at the schema level so every adapter's
|
|
757
|
+
* `parseServerConfig` boot path refuses the combination with a clear,
|
|
758
|
+
* field-named error instead of silently running an unconstrained
|
|
759
|
+
* auto-approver.
|
|
760
|
+
*/
|
|
761
|
+
export const ServerConfigSchema = ServerConfigSchemaObject.superRefine((data, ctx) => {
|
|
762
|
+
if (data.hostservAutoApproveVhosts === true && data.hostservAutoApproveAllowlist.length === 0) {
|
|
763
|
+
ctx.addIssue({
|
|
764
|
+
code: z.ZodIssueCode.custom,
|
|
765
|
+
message:
|
|
766
|
+
'Required and must be non-empty when hostservAutoApproveVhosts is true (auto-approve without an allowlist would let any identified user self-assign arbitrary vhosts); prefer queue mode (hostservAutoApproveVhosts: false)',
|
|
767
|
+
path: ['hostservAutoApproveAllowlist'],
|
|
768
|
+
});
|
|
769
|
+
}
|
|
371
770
|
});
|
|
372
771
|
|
|
373
772
|
/** Input type: what adapters supply (fields with defaults may be omitted). */
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared SASL credential hashing primitives.
|
|
3
3
|
*
|
|
4
|
-
* Both the AWS adapter (`
|
|
5
|
-
* (`
|
|
6
|
-
* These helpers live in `irc-core` so both
|
|
7
|
-
*
|
|
4
|
+
* Both the AWS adapter (`DynamoServicesStore`) and the CF adapter
|
|
5
|
+
* (`D1ServicesStore`) store NickServ/SASL credentials as **scrypt hashes**
|
|
6
|
+
* via the unified `ServicesStore`. These helpers live in `irc-core` so both
|
|
7
|
+
* adapters share one hashing implementation.
|
|
8
8
|
*
|
|
9
9
|
* Uses `node:crypto` (`scryptSync`), which is available on Node (AWS Lambda)
|
|
10
10
|
* natively and on Cloudflare Workers via the `nodejs_compat` flag. The public
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
17
|
-
import type { AccountStore, SaslPayload, SaslResult } from './ports.js';
|
|
18
17
|
|
|
19
18
|
const SCRYPT_KEY_LEN = 64;
|
|
20
19
|
const SALT_LEN = 16;
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
|
-
* A single
|
|
22
|
+
* A single credential stored as a scrypt hash.
|
|
24
23
|
*
|
|
25
|
-
* Every field is a persistence-backend attribute — this object IS the
|
|
26
|
-
*
|
|
24
|
+
* Every field is a persistence-backend attribute — this object IS the
|
|
25
|
+
* credential portion of a `nickserv_accounts` row (DynamoDB `Services`
|
|
26
|
+
* item / D1 `nickserv_accounts` row, keyed by the folded nick).
|
|
27
27
|
* `algorithm` is retained so future migrations to Argon2/bcrypt can be
|
|
28
28
|
* detected and handled gracefully by {@link verifyHashedPassword}.
|
|
29
29
|
*/
|
|
@@ -36,8 +36,9 @@ export interface HashedAccountCredential {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Hashes a plaintext password into a {@link HashedAccountCredential}
|
|
39
|
-
* suitable for writing to the
|
|
40
|
-
* `
|
|
39
|
+
* suitable for writing to the services backend
|
|
40
|
+
* (`ServicesStore.registerNick` stores the result on the registered-nick
|
|
41
|
+
* row).
|
|
41
42
|
*
|
|
42
43
|
* Uses `scryptSync` (memory-hard, GPU-resistant) with a random salt.
|
|
43
44
|
* Pass `{ salt }` for deterministic test scenarios; pass `{ keyLen }` to
|
|
@@ -78,47 +79,3 @@ export function verifyHashedPassword(password: string, entry: HashedAccountCrede
|
|
|
78
79
|
const computed = scryptSync(password, salt, expected.length);
|
|
79
80
|
return computed.length === expected.length && timingSafeEqual(computed, expected);
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Synchronous {@link AccountStore} backed by pre-loaded hashed credentials.
|
|
84
|
-
*
|
|
85
|
-
* Construct with entries produced by {@link hashAccountCredential} (for unit
|
|
86
|
-
* tests / seeding) or pre-loaded from a persistence backend at boot. Both
|
|
87
|
-
* adapters use this class:
|
|
88
|
-
*
|
|
89
|
-
* - AWS: `loadDynamoAccountStore` scans the `Accounts` table at Lambda
|
|
90
|
-
* cold start and constructs this store; `DynamoAccountStore` is a
|
|
91
|
-
* re-export alias for back-compat.
|
|
92
|
-
* - CF: `loadD1AccountStore` queries the D1 `accounts` table at
|
|
93
|
-
* `ConnectionDO` construction (mirroring the two-phase load pattern).
|
|
94
|
-
*
|
|
95
|
-
* The class has zero backend coupling — it works equally well with entries
|
|
96
|
-
* from any source. The `AccountStore.verify` port stays synchronous, so
|
|
97
|
-
* the async pre-load MUST complete before the first frame dispatches.
|
|
98
|
-
*/
|
|
99
|
-
export class HashedAccountStore implements AccountStore {
|
|
100
|
-
private readonly entries: ReadonlyMap<string, HashedAccountCredential>;
|
|
101
|
-
|
|
102
|
-
constructor(entries: ReadonlyArray<HashedAccountCredential>) {
|
|
103
|
-
this.entries = new Map(entries.map((e) => [e.account, e]));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
107
|
-
if (mech.toUpperCase() !== 'PLAIN' || payload.kind !== 'PLAIN') {
|
|
108
|
-
return { ok: false, reason: `unsupported mechanism: ${mech}` };
|
|
109
|
-
}
|
|
110
|
-
const entry = this.entries.get(payload.username);
|
|
111
|
-
if (entry === undefined) {
|
|
112
|
-
return { ok: false, reason: 'invalid credentials' };
|
|
113
|
-
}
|
|
114
|
-
if (!verifyHashedPassword(payload.password, entry)) {
|
|
115
|
-
return { ok: false, reason: 'invalid credentials' };
|
|
116
|
-
}
|
|
117
|
-
return { ok: true, account: payload.username };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Number of loaded accounts (diagnostics / logging). */
|
|
121
|
-
get size(): number {
|
|
122
|
-
return this.entries.size;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
@@ -132,6 +132,24 @@ export interface BroadcastWallopsEffect {
|
|
|
132
132
|
except?: ConnId;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Oper-only notice fanout. Emitted by the OPER reducer's per-IP lockout
|
|
137
|
+
* gate (`:<server> NOTICE * :OPER lockout triggered for <host>`) so every
|
|
138
|
+
* online operator learns a brute-force attempt is being throttled.
|
|
139
|
+
*
|
|
140
|
+
* Like {@link BroadcastWallopsEffect} this is global cross-connection
|
|
141
|
+
* fanout (not channel-scoped), but the recipient gate is user mode `+o`
|
|
142
|
+
* rather than `+w`: the bound runtime's `broadcastOperNotice` method owns
|
|
143
|
+
* the enumeration (in-memory: scan of the connection map; CF: registry
|
|
144
|
+
* shard walk; AWS: `Connections` table scan) and filters on
|
|
145
|
+
* `userModes.oper`.
|
|
146
|
+
*/
|
|
147
|
+
export interface BroadcastOperNoticeEffect {
|
|
148
|
+
tag: 'BroadcastOperNotice';
|
|
149
|
+
lines: RawLine[];
|
|
150
|
+
except?: ConnId;
|
|
151
|
+
}
|
|
152
|
+
|
|
135
153
|
/**
|
|
136
154
|
* NickServ nick-enforcement directive.
|
|
137
155
|
*
|
|
@@ -176,6 +194,7 @@ export type Effect =
|
|
|
176
194
|
| ApplyChannelDeltaEffect
|
|
177
195
|
| SendToNickEffect
|
|
178
196
|
| BroadcastWallopsEffect
|
|
197
|
+
| BroadcastOperNoticeEffect
|
|
179
198
|
| EnforceNickEffect;
|
|
180
199
|
|
|
181
200
|
/** Literal string tag of every {@link Effect} variant. */
|
|
@@ -245,6 +264,11 @@ export const Effect = {
|
|
|
245
264
|
if (except !== undefined) e.except = except;
|
|
246
265
|
return e;
|
|
247
266
|
},
|
|
267
|
+
broadcastOperNotice(lines: RawLine[], except?: ConnId): Effect {
|
|
268
|
+
const e: BroadcastOperNoticeEffect = { tag: 'BroadcastOperNotice', lines };
|
|
269
|
+
if (except !== undefined) e.except = except;
|
|
270
|
+
return e;
|
|
271
|
+
},
|
|
248
272
|
enforceNick(
|
|
249
273
|
conn: ConnId,
|
|
250
274
|
nick: Nick,
|
|
@@ -54,7 +54,8 @@ export interface FloodControlConfig {
|
|
|
54
54
|
* When omitted, every command is charged the flat {@link MESSAGE_COST}
|
|
55
55
|
* (`1`) — i.e. the original wrapper behaviour is preserved bit-for-bit.
|
|
56
56
|
* Compose with {@link defaultCommandCost} to exempt control-plane
|
|
57
|
-
* traffic (`PING`/`PONG`/`CAP`/`
|
|
57
|
+
* traffic (`PING`/`PONG`/`CAP`/`QUIT`; `AUTHENTICATE` is charged — see
|
|
58
|
+
* {@link EXEMPT_COMMANDS}).
|
|
58
59
|
*
|
|
59
60
|
* A cost of `0` neither decrements the bucket nor triggers the disconnect
|
|
60
61
|
* path; the inner reducer is called as normal. Negative costs are rejected
|
|
@@ -66,18 +67,17 @@ export interface FloodControlConfig {
|
|
|
66
67
|
/**
|
|
67
68
|
* Control-plane commands that mainstream ircds (charybdis, hybrid, Unreal) exempt
|
|
68
69
|
* from flood control. A client exchanging `PING`/`PONG` with the server or
|
|
69
|
-
* negotiating `CAP`
|
|
70
|
-
*
|
|
70
|
+
* negotiating `CAP` during registration must not flood itself out through no
|
|
71
|
+
* fault of its own.
|
|
72
|
+
*
|
|
73
|
+
* `AUTHENTICATE` is deliberately NOT exempt: SASL attempts consume the
|
|
74
|
+
* normal bucket so an in-connection SASL brute force is rate-limited by
|
|
75
|
+
* the same token bucket as every other command. The SASL reducer layers a
|
|
76
|
+
* per-connection failed-attempt lockout on top (see `commands/sasl.ts`).
|
|
71
77
|
*
|
|
72
78
|
* Override the set by composing your own cost function on top of this one.
|
|
73
79
|
*/
|
|
74
|
-
const EXEMPT_COMMANDS: ReadonlySet<string> = new Set([
|
|
75
|
-
'PING',
|
|
76
|
-
'PONG',
|
|
77
|
-
'CAP',
|
|
78
|
-
'AUTHENTICATE',
|
|
79
|
-
'QUIT',
|
|
80
|
-
]);
|
|
80
|
+
const EXEMPT_COMMANDS: ReadonlySet<string> = new Set(['PING', 'PONG', 'CAP', 'QUIT']);
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Ready-made per-command cost: `0` for the exempt control-plane set, `1`
|