serverless-ircd 0.2.0 → 0.4.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 +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +258 -18
- package/README.md +72 -30
- package/apps/aws-stack/README.md +2 -2
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +11 -1
- package/apps/cf-worker/wrangler.toml +69 -19
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +11 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +46 -3
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- package/package.json +19 -16
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +124 -34
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +62 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +43 -2
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +120 -9
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +13 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
- package/packages/aws-adapter/tests/account-store.test.ts +279 -0
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +75 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +194 -47
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +40 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +62 -0
- package/packages/cf-adapter/src/connection-do.ts +181 -31
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +58 -8
- package/packages/cf-adapter/src/index.ts +11 -9
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +40 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/worker/main.ts +9 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- 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 +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +8 -2
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +11 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +4 -3
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +26 -19
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +84 -3
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +240 -7
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +60 -1
- package/packages/irc-core/tests/account-store.test.ts +131 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +256 -19
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +662 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +170 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +182 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +21 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +617 -1
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +132 -2
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +9 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +79 -0
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/docs/AWS-Adapter-Architecture.md +0 -494
- package/docs/AWS-Deployment.md +0 -1107
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -1
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -440
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
* - nick registry: {@link ReserveNickEffect}, {@link ChangeNickEffect},
|
|
15
15
|
* {@link ReleaseNickEffect}
|
|
16
16
|
* - channel ownership: {@link ApplyChannelDeltaEffect}
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
*
|
|
18
|
+
* Lookup-shaped runtime methods (`lookupNick`, `getConnectionInfo`,
|
|
19
|
+
* `getChannelSnapshot`) are consumed directly inside `dispatch`'s
|
|
20
|
+
* broadcast helper and the actor — they are request/response and cannot
|
|
21
|
+
* be modelled as pure fire-and-forget effects, so no Effect variants
|
|
22
|
+
* wrap them.
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
25
|
import type { ChanName, ChannelDelta } from './state/channel.js';
|
|
@@ -71,21 +75,6 @@ export interface ApplyChannelDeltaEffect {
|
|
|
71
75
|
delta: ChannelDelta;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
export interface LookupNickEffect {
|
|
75
|
-
tag: 'LookupNick';
|
|
76
|
-
nick: Nick;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface GetConnectionInfoEffect {
|
|
80
|
-
tag: 'GetConnectionInfo';
|
|
81
|
-
conn: ConnId;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface GetChannelSnapshotEffect {
|
|
85
|
-
tag: 'GetChannelSnapshot';
|
|
86
|
-
chan: ChanName;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
78
|
/**
|
|
90
79
|
* Routes `lines` to whichever connection currently owns `nick`. If the nick
|
|
91
80
|
* is offline, `notFoundLines` (when supplied) is sent back to `sender` —
|
|
@@ -109,9 +98,6 @@ export type Effect =
|
|
|
109
98
|
| ChangeNickEffect
|
|
110
99
|
| ReleaseNickEffect
|
|
111
100
|
| ApplyChannelDeltaEffect
|
|
112
|
-
| LookupNickEffect
|
|
113
|
-
| GetConnectionInfoEffect
|
|
114
|
-
| GetChannelSnapshotEffect
|
|
115
101
|
| SendToNickEffect;
|
|
116
102
|
|
|
117
103
|
/** Literal string tag of every {@link Effect} variant. */
|
|
@@ -167,15 +153,6 @@ export const Effect = {
|
|
|
167
153
|
applyChannelDelta(chan: ChanName, delta: ChannelDelta): Effect {
|
|
168
154
|
return { tag: 'ApplyChannelDelta', chan, delta };
|
|
169
155
|
},
|
|
170
|
-
lookupNick(nick: Nick): Effect {
|
|
171
|
-
return { tag: 'LookupNick', nick };
|
|
172
|
-
},
|
|
173
|
-
getConnectionInfo(conn: ConnId): Effect {
|
|
174
|
-
return { tag: 'GetConnectionInfo', conn };
|
|
175
|
-
},
|
|
176
|
-
getChannelSnapshot(chan: ChanName): Effect {
|
|
177
|
-
return { tag: 'GetChannelSnapshot', chan };
|
|
178
|
-
},
|
|
179
156
|
sendToNick(nick: Nick, sender: ConnId, lines: RawLine[], notFoundLines?: RawLine[]): Effect {
|
|
180
157
|
const e: SendToNickEffect = { tag: 'SendToNick', nick, sender, lines };
|
|
181
158
|
if (notFoundLines !== undefined) e.notFoundLines = notFoundLines;
|
|
@@ -10,14 +10,17 @@ export * from './caps/index.js';
|
|
|
10
10
|
export * from './commands/index.js';
|
|
11
11
|
export * from './effects.js';
|
|
12
12
|
export * from './ports.js';
|
|
13
|
+
export * from './credential-hashing.js';
|
|
13
14
|
export * from './flood-control.js';
|
|
14
15
|
export * from './cloak.js';
|
|
16
|
+
export * from './case-fold.js';
|
|
15
17
|
export * from './admission.js';
|
|
16
18
|
export * from './config.js';
|
|
17
19
|
export type {
|
|
18
20
|
BuildCtxOptions,
|
|
19
21
|
CloakingConfig,
|
|
20
22
|
Ctx,
|
|
23
|
+
OperCred,
|
|
21
24
|
Reducer,
|
|
22
25
|
ReducerResult,
|
|
23
26
|
ServerConfig,
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* can pin the timeline and identity space (PLAN §8 determinism rules).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { caseFold } from './case-fold.js';
|
|
10
|
+
|
|
9
11
|
/** Read-only wall clock, epoch milliseconds. */
|
|
10
12
|
export interface Clock {
|
|
11
13
|
now(): number;
|
|
@@ -55,7 +57,7 @@ export interface IdFactory {
|
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* Production id factory. Generates UUIDv4-shaped strings without relying
|
|
58
|
-
* on `crypto` so the same code runs on Node
|
|
60
|
+
* on `crypto` so the same code runs on Node 24, Cloudflare Workers, and
|
|
59
61
|
* Lambda without per-platform type wrangling. Uniqueness is statistical;
|
|
60
62
|
* for cryptographic IDs, adapters can supply their own {@link IdFactory}.
|
|
61
63
|
*/
|
|
@@ -169,7 +171,8 @@ export const EmptyMotdProvider: MotdProvider = new StaticMotdProvider([]);
|
|
|
169
171
|
*/
|
|
170
172
|
export type SaslPayload =
|
|
171
173
|
| { kind: 'PLAIN'; username: string; password: string }
|
|
172
|
-
| { kind: 'RAW'; data: string }
|
|
174
|
+
| { kind: 'RAW'; data: string }
|
|
175
|
+
| { kind: 'EXTERNAL'; identity: string };
|
|
173
176
|
|
|
174
177
|
/**
|
|
175
178
|
* Outcome of an {@link AccountStore.verify} call: either the canonical
|
|
@@ -192,6 +195,134 @@ export interface AccountStore {
|
|
|
192
195
|
verify(mech: string, payload: SaslPayload): SaslResult;
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
/**
|
|
199
|
+
* One SASL PLAIN credential entry seeded into an {@link InMemoryAccountStore}.
|
|
200
|
+
* The `username` is also the canonical account name recorded on the
|
|
201
|
+
* connection for `extended-join` / `account-tag`.
|
|
202
|
+
*/
|
|
203
|
+
export interface SaslAccountCredential {
|
|
204
|
+
username: string;
|
|
205
|
+
password: string;
|
|
206
|
+
/**
|
|
207
|
+
* Optional mTLS client-certificate subject (e.g. `CN=alice` or a
|
|
208
|
+
* fingerprint hash). When set, SASL `EXTERNAL` authentications that
|
|
209
|
+
* present this subject are mapped to {@link username} as the account.
|
|
210
|
+
*/
|
|
211
|
+
certSubject?: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Reference in-memory {@link AccountStore} backed by a static list of PLAIN
|
|
216
|
+
* credentials seeded at construction.
|
|
217
|
+
*
|
|
218
|
+
* Mirrors the {@link InMemoryMessageStore} / {@link StaticMotdProvider}
|
|
219
|
+
* pattern: the port is synchronous, so adapters that need an async backend
|
|
220
|
+
* (D1, DynamoDB, Secrets Manager) pre-load their credentials into this store
|
|
221
|
+
* at boot. All three adapters (local-cli, CF, AWS) use this as the minimum
|
|
222
|
+
* viable `AccountStore`; a persistent variant (DynamoDB `Accounts` table, D1)
|
|
223
|
+
* is a documented follow-up that swaps in by replacing the construction call.
|
|
224
|
+
*
|
|
225
|
+
* Password comparison is constant-time to avoid the early-return timing
|
|
226
|
+
* oracle a naive `===` would expose. Only `PLAIN` is verified; any other
|
|
227
|
+
* mechanism returns failure (matching the reducer's mech gating).
|
|
228
|
+
*/
|
|
229
|
+
export class InMemoryAccountStore implements AccountStore {
|
|
230
|
+
private readonly creds: ReadonlyMap<string, string>;
|
|
231
|
+
private readonly certMap: ReadonlyMap<string, string>;
|
|
232
|
+
|
|
233
|
+
constructor(credentials: ReadonlyArray<SaslAccountCredential>) {
|
|
234
|
+
this.creds = new Map(credentials.map((c) => [c.username, c.password]));
|
|
235
|
+
this.certMap = new Map(
|
|
236
|
+
credentials
|
|
237
|
+
.filter((c) => c.certSubject !== undefined)
|
|
238
|
+
.map((c) => [c.certSubject as string, c.username]),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
243
|
+
const m = mech.toUpperCase();
|
|
244
|
+
if (m === 'PLAIN' && payload.kind === 'PLAIN') {
|
|
245
|
+
const expected = this.creds.get(payload.username);
|
|
246
|
+
if (expected === undefined) {
|
|
247
|
+
return { ok: false, reason: 'invalid credentials' };
|
|
248
|
+
}
|
|
249
|
+
if (!constantTimeEquals(payload.password, expected)) {
|
|
250
|
+
return { ok: false, reason: 'invalid credentials' };
|
|
251
|
+
}
|
|
252
|
+
return { ok: true, account: payload.username };
|
|
253
|
+
}
|
|
254
|
+
if (m === 'EXTERNAL' && payload.kind === 'EXTERNAL') {
|
|
255
|
+
const account = this.certMap.get(payload.identity);
|
|
256
|
+
if (account === undefined) {
|
|
257
|
+
return { ok: false, reason: 'untrusted certificate' };
|
|
258
|
+
}
|
|
259
|
+
return { ok: true, account };
|
|
260
|
+
}
|
|
261
|
+
return { ok: false, reason: `unsupported mechanism: ${mech}` };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Constant-time string equality. Compares every byte regardless of early
|
|
267
|
+
* mismatches so a remote attacker cannot short-circuit the comparison via a
|
|
268
|
+
* timing side-channel. Returns `false` when the lengths differ (the length
|
|
269
|
+
* is not considered secret for SASL PLAIN passwords).
|
|
270
|
+
*/
|
|
271
|
+
function constantTimeEquals(a: string, b: string): boolean {
|
|
272
|
+
if (a.length !== b.length) return false;
|
|
273
|
+
let diff = 0;
|
|
274
|
+
for (let i = 0; i < a.length; i++) {
|
|
275
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
276
|
+
}
|
|
277
|
+
return diff === 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// mTLS identity (SASL EXTERNAL)
|
|
282
|
+
// ============================================================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Port that resolves the verified client-certificate identity for a
|
|
286
|
+
* connection.
|
|
287
|
+
*
|
|
288
|
+
* At the edge (CF API Shield mTLS, AWS API Gateway custom-domain mTLS) the
|
|
289
|
+
* platform terminates TLS, validates the client certificate against the
|
|
290
|
+
* configured trust store, and surfaces the certificate subject to the
|
|
291
|
+
* Worker/Lambda. The adapter captures this at connection-admission time
|
|
292
|
+
* and stores it keyed by connection ID.
|
|
293
|
+
*
|
|
294
|
+
* The method is **synchronous** — adapters pre-populate the identity at
|
|
295
|
+
* admission time (before the first reducer runs), so the SASL EXTERNAL
|
|
296
|
+
* reducer can look it up without awaiting. When no client certificate was
|
|
297
|
+
* presented (or mTLS is not configured), `getIdentity` returns `undefined`
|
|
298
|
+
* and EXTERNAL authentication fails with `904`.
|
|
299
|
+
*/
|
|
300
|
+
export interface MtlsIdentityProvider {
|
|
301
|
+
/**
|
|
302
|
+
* @returns the verified client-cert subject for `connId`, or `undefined`
|
|
303
|
+
* when no trusted certificate was presented.
|
|
304
|
+
*/
|
|
305
|
+
getIdentity(connId: string): string | undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Reference in-memory {@link MtlsIdentityProvider} backed by a static map
|
|
310
|
+
* of connection IDs to certificate subjects. Useful for tests and the
|
|
311
|
+
* local-cli adapter; cloud adapters replace this with a lookup keyed by
|
|
312
|
+
* the platform's connection identifier.
|
|
313
|
+
*/
|
|
314
|
+
export class InMemoryMtlsIdentityProvider implements MtlsIdentityProvider {
|
|
315
|
+
private readonly identities: ReadonlyMap<string, string>;
|
|
316
|
+
|
|
317
|
+
constructor(entries: ReadonlyArray<{ connId: string; identity: string }>) {
|
|
318
|
+
this.identities = new Map(entries.map((e) => [e.connId, e.identity]));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
getIdentity(connId: string): string | undefined {
|
|
322
|
+
return this.identities.get(connId);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
195
326
|
// ============================================================================
|
|
196
327
|
// Chat-history persistence (IRCv3 draft/chathistory)
|
|
197
328
|
// ============================================================================
|
|
@@ -301,7 +432,7 @@ export class InMemoryMessageStore implements MessageStore {
|
|
|
301
432
|
}
|
|
302
433
|
|
|
303
434
|
record(message: StoredMessage): void {
|
|
304
|
-
const key = message.chan
|
|
435
|
+
const key = caseFold('rfc1459', message.chan);
|
|
305
436
|
let arr = this.byChan.get(key);
|
|
306
437
|
if (arr === undefined) {
|
|
307
438
|
arr = [];
|
|
@@ -315,7 +446,7 @@ export class InMemoryMessageStore implements MessageStore {
|
|
|
315
446
|
|
|
316
447
|
query(q: HistoryQuery): StoredMessage[] {
|
|
317
448
|
if (q.limit <= 0) return [];
|
|
318
|
-
const arr = this.byChan.get(q.chan
|
|
449
|
+
const arr = this.byChan.get(caseFold('rfc1459', q.chan));
|
|
319
450
|
if (arr === undefined || arr.length === 0) return [];
|
|
320
451
|
|
|
321
452
|
switch (q.direction) {
|
|
@@ -335,14 +466,14 @@ export class InMemoryMessageStore implements MessageStore {
|
|
|
335
466
|
}
|
|
336
467
|
|
|
337
468
|
hasMsgid(chan: string, msgid: string): boolean {
|
|
338
|
-
const arr = this.byChan.get(chan
|
|
469
|
+
const arr = this.byChan.get(caseFold('rfc1459', chan));
|
|
339
470
|
if (arr === undefined) return false;
|
|
340
471
|
return arr.some((m) => m.msgid === msgid);
|
|
341
472
|
}
|
|
342
473
|
|
|
343
474
|
recent(chan: string, sinceMsgid: string | undefined, limit: number): StoredMessage[] {
|
|
344
475
|
if (limit <= 0) return [];
|
|
345
|
-
const arr = this.byChan.get(chan
|
|
476
|
+
const arr = this.byChan.get(caseFold('rfc1459', chan));
|
|
346
477
|
if (arr === undefined || arr.length === 0) return [];
|
|
347
478
|
let start = 0;
|
|
348
479
|
if (sinceMsgid !== undefined) {
|
|
@@ -437,6 +568,108 @@ function tail<T>(arr: readonly T[], n: number): T[] {
|
|
|
437
568
|
return arr.slice(start);
|
|
438
569
|
}
|
|
439
570
|
|
|
571
|
+
// ============================================================================
|
|
572
|
+
// Nick history (WHOWAS)
|
|
573
|
+
// ============================================================================
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* One persisted sign-off record for a nick, written by the QUIT and
|
|
577
|
+
* post-registration NICK-change reducers and replayed by `WHOWAS`.
|
|
578
|
+
*
|
|
579
|
+
* `nick` preserves the original case the client used; `connId` is the
|
|
580
|
+
* connection that owned the nick at sign-off. `username` / `hostname` /
|
|
581
|
+
* `realname` mirror the {@link ConnectionState} fields at sign-off time
|
|
582
|
+
* (optional because a never-fully-registered connection may still quit with
|
|
583
|
+
* a nick). `signoffTime` is epoch milliseconds from the injected
|
|
584
|
+
* {@link Clock}.
|
|
585
|
+
*/
|
|
586
|
+
export interface NickHistoryEntry {
|
|
587
|
+
nick: string;
|
|
588
|
+
connId: string;
|
|
589
|
+
username?: string;
|
|
590
|
+
hostname?: string;
|
|
591
|
+
realname?: string;
|
|
592
|
+
signoffTime: number;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Persistence port for the IRC `WHOWAS` command.
|
|
597
|
+
*
|
|
598
|
+
* Mirrors the {@link MessageStore} pattern: the port is **synchronous**
|
|
599
|
+
* (reducers must stay pure and free of IO) and injected via {@link Ctx}.
|
|
600
|
+
* Adapters that need async storage (D1, DynamoDB) pre-load the relevant
|
|
601
|
+
* slice into a synchronously-readable store; Phase 8 ships the in-memory
|
|
602
|
+
* reference implementation only.
|
|
603
|
+
*
|
|
604
|
+
* `query` always returns matches most-recent-first (the order `WHOWAS`
|
|
605
|
+
* emits `314 RPL_WHOWASUSER` lines in), capped at `count`.
|
|
606
|
+
*/
|
|
607
|
+
export interface NickHistoryStore {
|
|
608
|
+
/** Records a sign-off entry for a nick (TTL eviction is impl-defined). */
|
|
609
|
+
record(entry: NickHistoryEntry): void;
|
|
610
|
+
/** Returns up to `count` most-recent entries for `nick` (case-insensitive). */
|
|
611
|
+
query(nick: string, count: number): NickHistoryEntry[];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Default retention window for nick-history entries: 24 hours. */
|
|
615
|
+
const DEFAULT_NICK_HISTORY_MAX_AGE = 24 * 60 * 60 * 1000;
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Reference in-memory {@link NickHistoryStore} backed by a per-nick
|
|
619
|
+
* chronological array with a configurable TTL.
|
|
620
|
+
*
|
|
621
|
+
* Used by the local CLI and by tests. Each nick keeps its sign-off records
|
|
622
|
+
* in arrival order; entries older than `maxAge` (measured against the
|
|
623
|
+
* injected {@link Clock}) are dropped lazily on `record` and `query` so the
|
|
624
|
+
* per-nick buffers do not grow unbounded.
|
|
625
|
+
*
|
|
626
|
+
* Nicks are keyed via {@link caseFold} (`rfc1459`) so lookups are
|
|
627
|
+
* case-insensitive, matching the IRC nick-comparison semantics advertised
|
|
628
|
+
* via `005 RPL_ISUPPORT CASEMAPPING=rfc1459`.
|
|
629
|
+
*/
|
|
630
|
+
export class InMemoryNickHistoryStore implements NickHistoryStore {
|
|
631
|
+
private readonly byNick = new Map<string, NickHistoryEntry[]>();
|
|
632
|
+
private readonly clock: Clock;
|
|
633
|
+
private readonly maxAge: number;
|
|
634
|
+
|
|
635
|
+
constructor(clock: Clock, opts?: { maxAge?: number }) {
|
|
636
|
+
this.clock = clock;
|
|
637
|
+
this.maxAge = opts?.maxAge ?? DEFAULT_NICK_HISTORY_MAX_AGE;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
record(entry: NickHistoryEntry): void {
|
|
641
|
+
const key = caseFold('rfc1459', entry.nick);
|
|
642
|
+
let arr = this.byNick.get(key);
|
|
643
|
+
if (arr === undefined) {
|
|
644
|
+
arr = [];
|
|
645
|
+
this.byNick.set(key, arr);
|
|
646
|
+
}
|
|
647
|
+
this.purgeExpired(arr);
|
|
648
|
+
arr.push(entry);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
query(nick: string, count: number): NickHistoryEntry[] {
|
|
652
|
+
if (count <= 0) return [];
|
|
653
|
+
const arr = this.byNick.get(caseFold('rfc1459', nick));
|
|
654
|
+
if (arr === undefined) return [];
|
|
655
|
+
this.purgeExpired(arr);
|
|
656
|
+
if (arr.length === 0) return [];
|
|
657
|
+
return arr.slice(Math.max(0, arr.length - count)).reverse();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Drops entries whose `signoffTime` is older than `now - maxAge`, in place.
|
|
662
|
+
* Entries arrive in chronological order (append-only), so the first alive
|
|
663
|
+
* entry marks the boundary; everything before it is expired.
|
|
664
|
+
*/
|
|
665
|
+
private purgeExpired(arr: NickHistoryEntry[]): void {
|
|
666
|
+
const cutoff = this.clock.now() - this.maxAge;
|
|
667
|
+
let firstAlive = arr.findIndex((e) => e.signoffTime >= cutoff);
|
|
668
|
+
if (firstAlive === -1) firstAlive = arr.length;
|
|
669
|
+
arr.splice(0, firstAlive);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
440
673
|
// ============================================================================
|
|
441
674
|
// Observability — structured logger
|
|
442
675
|
// ============================================================================
|
|
@@ -790,7 +1023,7 @@ export class ConsoleLogger implements Logger {
|
|
|
790
1023
|
|
|
791
1024
|
/** Returns the global `console` as a {@link ConsoleLoggerSinks}. */
|
|
792
1025
|
function globalThisConsoleSinks(): ConsoleLoggerSinks {
|
|
793
|
-
// `globalThis.console` is universal across Node
|
|
1026
|
+
// `globalThis.console` is universal across Node 24, Workers, and Lambda.
|
|
794
1027
|
// Reference it lazily so tests that stub the logger do not need to
|
|
795
1028
|
// replace the global.
|
|
796
1029
|
const c = (globalThis as { console?: GlobalConsole }).console;
|
|
@@ -13,7 +13,7 @@ export const Numerics = {
|
|
|
13
13
|
RPL_ISUPPORT: 5,
|
|
14
14
|
RPL_BOUNCE: 10,
|
|
15
15
|
|
|
16
|
-
// Capability / stats admin
|
|
16
|
+
// Capability / stats admin / version
|
|
17
17
|
RPL_ADMINME: 256,
|
|
18
18
|
|
|
19
19
|
// Away / userhost / ison
|
|
@@ -23,7 +23,10 @@ export const Numerics = {
|
|
|
23
23
|
RPL_UNAWAY: 305,
|
|
24
24
|
RPL_NOWAWAY: 306,
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// Version
|
|
27
|
+
RPL_VERSION: 351,
|
|
28
|
+
|
|
29
|
+
// WHOIS / WHOWAS
|
|
27
30
|
RPL_WHOISUSER: 311,
|
|
28
31
|
RPL_WHOISSERVER: 312,
|
|
29
32
|
RPL_WHOISOPERATOR: 313,
|
|
@@ -31,6 +34,7 @@ export const Numerics = {
|
|
|
31
34
|
RPL_ENDOFWHOIS: 318,
|
|
32
35
|
RPL_WHOISCHANNELS: 319,
|
|
33
36
|
RPL_WHOISACCOUNT: 330,
|
|
37
|
+
RPL_WHOWASUSER: 314,
|
|
34
38
|
|
|
35
39
|
// LIST
|
|
36
40
|
RPL_LISTSTART: 321,
|
|
@@ -57,7 +61,8 @@ export const Numerics = {
|
|
|
57
61
|
RPL_NAMREPLY: 353,
|
|
58
62
|
RPL_ENDOFNAMES: 366,
|
|
59
63
|
|
|
60
|
-
// Links / ban / whowas
|
|
64
|
+
// Links / ban / whowas. RPL_LINKS/RPL_ENDOFLINKS retained for the deferred
|
|
65
|
+
// LINKS verb (S2S server list — PLAN non-goal; see actor default-route triage).
|
|
61
66
|
RPL_LINKS: 364,
|
|
62
67
|
RPL_ENDOFLINKS: 365,
|
|
63
68
|
RPL_BANLIST: 367,
|
|
@@ -71,7 +76,8 @@ export const Numerics = {
|
|
|
71
76
|
RPL_MOTDSTART: 375,
|
|
72
77
|
RPL_ENDOFMOTD: 376,
|
|
73
78
|
|
|
74
|
-
// Oper
|
|
79
|
+
// Oper. RPL_REHASHING retained for the deferred REHASH verb (no runtime
|
|
80
|
+
// config-reload path in serverless; see actor default-route triage).
|
|
75
81
|
RPL_YOUREOPER: 381,
|
|
76
82
|
RPL_REHASHING: 382,
|
|
77
83
|
RPL_YOURESERVICE: 383,
|
|
@@ -96,7 +102,7 @@ export const Numerics = {
|
|
|
96
102
|
ERR_TOOMANYCHANNELS: 405,
|
|
97
103
|
ERR_WASNOSUCHNICK: 406,
|
|
98
104
|
ERR_TOOMANYTARGETS: 407,
|
|
99
|
-
ERR_NOSUCHSERVICE: 408,
|
|
105
|
+
ERR_NOSUCHSERVICE: 408, // retained: deferred SERVICE verb (RFC 2812 obsolete)
|
|
100
106
|
ERR_INVALIDCAPCMD: 410,
|
|
101
107
|
ERR_NORECIPIENT: 411,
|
|
102
108
|
ERR_NOTEXTTOSEND: 412,
|
|
@@ -115,8 +121,8 @@ export const Numerics = {
|
|
|
115
121
|
ERR_NOTONCHANNEL: 442,
|
|
116
122
|
ERR_USERONCHANNEL: 443,
|
|
117
123
|
ERR_NOLOGIN: 444,
|
|
118
|
-
ERR_SUMMONDISABLED: 445,
|
|
119
|
-
ERR_USERSDISABLED: 446,
|
|
124
|
+
ERR_SUMMONDISABLED: 445, // retained: deferred SUMMON verb (RFC 2812 obsolete)
|
|
125
|
+
ERR_USERSDISABLED: 446, // retained: deferred USERS verb (RFC 2812 obsolete)
|
|
120
126
|
ERR_NOTREGISTERED: 451,
|
|
121
127
|
ERR_NEEDMOREPARAMS: 461,
|
|
122
128
|
ERR_ALREADYREGISTRED: 462,
|
|
@@ -134,7 +140,7 @@ export const Numerics = {
|
|
|
134
140
|
ERR_BANLISTFULL: 478,
|
|
135
141
|
ERR_NOPRIVILEGES: 481,
|
|
136
142
|
ERR_CHANOPRIVSNEEDED: 482,
|
|
137
|
-
ERR_CANTKILLSERVER: 483,
|
|
143
|
+
ERR_CANTKILLSERVER: 483, // retained: deferred SQUIT verb (S2S, PLAN non-goal)
|
|
138
144
|
ERR_NOOPERHOST: 491,
|
|
139
145
|
ERR_UMODEUNKNOWNFLAG: 501,
|
|
140
146
|
ERR_USERSDONTMATCH: 502,
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Effect } from './effects.js';
|
|
10
10
|
import type { FloodControlConfig } from './flood-control.js';
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
AccountStore,
|
|
13
|
+
Clock,
|
|
14
|
+
IdFactory,
|
|
15
|
+
MessageStore,
|
|
16
|
+
MotdProvider,
|
|
17
|
+
MtlsIdentityProvider,
|
|
18
|
+
NickHistoryStore,
|
|
19
|
+
} from './ports.js';
|
|
12
20
|
import type { IrcMessage } from './protocol/messages.js';
|
|
13
21
|
import type { ConnectionState } from './state/connection.js';
|
|
14
22
|
|
|
@@ -25,6 +33,17 @@ export interface CloakingConfig {
|
|
|
25
33
|
readonly cloakedSuffix?: string | undefined;
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A single IRC operator credential. The {@link operReducer} consults the
|
|
38
|
+
* configured list to authenticate `OPER <name> <password>`. Both fields are
|
|
39
|
+
* compared verbatim; adapters are responsible for any hashing at the auth
|
|
40
|
+
* boundary if they layer one on top.
|
|
41
|
+
*/
|
|
42
|
+
export interface OperCred {
|
|
43
|
+
readonly user: string;
|
|
44
|
+
readonly password: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
/**
|
|
29
48
|
* Per-deployment server configuration. Adapters load this from a KV store
|
|
30
49
|
* (CF) or Secrets Manager / SSM (AWS); the core only reads it.
|
|
@@ -35,6 +54,18 @@ export interface CloakingConfig {
|
|
|
35
54
|
export interface ServerConfig {
|
|
36
55
|
serverName: string;
|
|
37
56
|
networkName: string;
|
|
57
|
+
/**
|
|
58
|
+
* Server version surfaced in `002`/`004`/`351`/`371`. When undefined the
|
|
59
|
+
* reducer falls back to {@link DEFAULT_SERVER_VERSION} (the irc-core
|
|
60
|
+
* package version); adapters inject the deploy-time version via config.
|
|
61
|
+
*/
|
|
62
|
+
readonly serverVersion?: string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* "Created" text for `003 RPL_CREATED`. A string is used verbatim; a
|
|
65
|
+
* number is treated as epoch-ms and formatted as a UTC timestamp. When
|
|
66
|
+
* undefined the reducer falls back to {@link DEFAULT_CREATED_TEXT}.
|
|
67
|
+
*/
|
|
68
|
+
readonly createdAt?: string | number | undefined;
|
|
38
69
|
/**
|
|
39
70
|
* Server password gate. Empty string or undefined disables enforcement.
|
|
40
71
|
* When set, registration refuses to complete until the connection
|
|
@@ -53,6 +84,17 @@ export interface ServerConfig {
|
|
|
53
84
|
* (see `serverPassword` note).
|
|
54
85
|
*/
|
|
55
86
|
readonly cloaking?: CloakingConfig | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* IRC operator credentials consulted by the `OPER` reducer. `undefined`
|
|
89
|
+
* (or an empty array) disables oper authentication entirely — an `OPER`
|
|
90
|
+
* attempt then receives `491 ERR_NOOPERHOST`.
|
|
91
|
+
*
|
|
92
|
+
* Typed `ReadonlyArray<OperCred> | undefined` to mirror Zod's `.optional()`
|
|
93
|
+
* output under `exactOptionalPropertyTypes: true`; the parsed config always
|
|
94
|
+
* supplies an array (defaulting to `[]`), so a parsed config is assignable
|
|
95
|
+
* here.
|
|
96
|
+
*/
|
|
97
|
+
readonly operCreds?: ReadonlyArray<OperCred> | undefined;
|
|
56
98
|
maxChannelsPerUser: number;
|
|
57
99
|
maxTargetsPerCommand: number;
|
|
58
100
|
maxListEntries: number;
|
|
@@ -93,6 +135,12 @@ export interface Ctx {
|
|
|
93
135
|
readonly motd: MotdProvider;
|
|
94
136
|
/** SASL account verification source (absent when no accounts are configured). */
|
|
95
137
|
readonly accounts?: AccountStore;
|
|
138
|
+
/**
|
|
139
|
+
* mTLS client-cert identity source for SASL EXTERNAL (absent when mTLS
|
|
140
|
+
* is not configured). When present, `AUTHENTICATE EXTERNAL` resolves the
|
|
141
|
+
* connection's verified certificate subject through this port.
|
|
142
|
+
*/
|
|
143
|
+
readonly mtlsIdentity?: MtlsIdentityProvider;
|
|
96
144
|
/**
|
|
97
145
|
* Chat-history persistence source (absent when chathistory playback is
|
|
98
146
|
* disabled for this deployment). When present, the PRIVMSG/NOTICE channel
|
|
@@ -100,6 +148,13 @@ export interface Ctx {
|
|
|
100
148
|
* cap-enabled joiners.
|
|
101
149
|
*/
|
|
102
150
|
readonly messages?: MessageStore;
|
|
151
|
+
/**
|
|
152
|
+
* Nick-history source for the `WHOWAS` command (absent when WHOWAS
|
|
153
|
+
* history retention is disabled for this deployment). When present, the
|
|
154
|
+
* QUIT and post-registration NICK-change reducers record sign-off
|
|
155
|
+
* entries and the WHOWAS reducer replays them.
|
|
156
|
+
*/
|
|
157
|
+
readonly history?: NickHistoryStore;
|
|
103
158
|
/** The connection invoking the command. Reducers mutate this freely. */
|
|
104
159
|
readonly connection: ConnectionState;
|
|
105
160
|
/** Convenience: `connection.id`. */
|
|
@@ -125,7 +180,9 @@ export interface BuildCtxOptions {
|
|
|
125
180
|
ids: IdFactory;
|
|
126
181
|
motd: MotdProvider;
|
|
127
182
|
accounts?: AccountStore;
|
|
183
|
+
mtlsIdentity?: MtlsIdentityProvider;
|
|
128
184
|
messages?: MessageStore;
|
|
185
|
+
history?: NickHistoryStore;
|
|
129
186
|
connection: ConnectionState;
|
|
130
187
|
}
|
|
131
188
|
|
|
@@ -141,7 +198,9 @@ export function buildCtx(opts: BuildCtxOptions): Ctx {
|
|
|
141
198
|
connection: opts.connection,
|
|
142
199
|
connId: opts.connection.id,
|
|
143
200
|
...(opts.accounts !== undefined ? { accounts: opts.accounts } : {}),
|
|
201
|
+
...(opts.mtlsIdentity !== undefined ? { mtlsIdentity: opts.mtlsIdentity } : {}),
|
|
144
202
|
...(opts.messages !== undefined ? { messages: opts.messages } : {}),
|
|
203
|
+
...(opts.history !== undefined ? { history: opts.history } : {}),
|
|
145
204
|
};
|
|
146
205
|
return ctx;
|
|
147
206
|
}
|