serverless-ircd 0.3.0 → 0.5.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 +349 -18
- package/README.md +132 -30
- package/apps/aws-stack/README.md +6 -5
- 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 +150 -10
- package/apps/aws-stack/tests/stack.test.ts +145 -4
- 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/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +149 -24
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +75 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +96 -9
- package/packages/aws-adapter/src/handlers/default.ts +98 -7
- package/packages/aws-adapter/src/handlers/index.ts +109 -4
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +8 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- 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/aws-integration.test.ts +1 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +174 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +302 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- 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 +5 -1
- package/packages/cf-adapter/src/cf-runtime.ts +68 -5
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +278 -85
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +54 -11
- package/packages/cf-adapter/src/index.ts +11 -8
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +12 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
- package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- 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-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- 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/registry-do.test.ts +4 -4
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +15 -8
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/wrangler.test.toml +15 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +24 -3
- 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 +17 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +6 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +4 -8
- package/packages/irc-core/src/commands/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- 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/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +65 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +33 -30
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +360 -12
- package/packages/irc-core/src/protocol/numerics.ts +48 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +46 -2
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- 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/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +57 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- 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/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +102 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- 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 +393 -16
- package/packages/irc-server/src/dispatch.ts +1 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +15 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +104 -0
- package/packages/irc-server/tests/actor.test.ts +1489 -4
- package/packages/irc-server/tests/dispatch.test.ts +37 -17
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +5 -0
- package/packages/irc-server/tests/runtime.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 +73 -9
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +141 -3
- 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 +10 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +9 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +107 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -443
|
@@ -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` —
|
|
@@ -100,6 +89,26 @@ export interface SendToNickEffect {
|
|
|
100
89
|
notFoundLines?: RawLine[];
|
|
101
90
|
}
|
|
102
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Global cross-connection fanout for `WALLOPS`. Unlike
|
|
94
|
+
* {@link BroadcastEffect} (channel-scoped via the roster), this reaches
|
|
95
|
+
* every live connection whose `+w` user mode is set, regardless of shared
|
|
96
|
+
* channel membership. `except` skips a single connection — by convention
|
|
97
|
+
* the originator, so an oper never receives their own wallops back
|
|
98
|
+
* (matching NOTICE/PRIVMSG channel-broadcast semantics).
|
|
99
|
+
*
|
|
100
|
+
* The reducer does not know which connections carry `+w`; the bound
|
|
101
|
+
* runtime's `broadcastWallops` method owns that enumeration. On the
|
|
102
|
+
* in-memory runtime that is a scan of its connection map; on CF it walks
|
|
103
|
+
* the registry shards; on AWS it scans the `Connections` table. Each
|
|
104
|
+
* adapter applies its own cost cap (documented at the method).
|
|
105
|
+
*/
|
|
106
|
+
export interface BroadcastWallopsEffect {
|
|
107
|
+
tag: 'BroadcastWallops';
|
|
108
|
+
lines: RawLine[];
|
|
109
|
+
except?: ConnId;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
/** Discriminated union of every effect the dispatch interpreter knows. */
|
|
104
113
|
export type Effect =
|
|
105
114
|
| SendEffect
|
|
@@ -109,10 +118,8 @@ export type Effect =
|
|
|
109
118
|
| ChangeNickEffect
|
|
110
119
|
| ReleaseNickEffect
|
|
111
120
|
| ApplyChannelDeltaEffect
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
| GetChannelSnapshotEffect
|
|
115
|
-
| SendToNickEffect;
|
|
121
|
+
| SendToNickEffect
|
|
122
|
+
| BroadcastWallopsEffect;
|
|
116
123
|
|
|
117
124
|
/** Literal string tag of every {@link Effect} variant. */
|
|
118
125
|
export type EffectTag = Effect['tag'];
|
|
@@ -167,18 +174,14 @@ export const Effect = {
|
|
|
167
174
|
applyChannelDelta(chan: ChanName, delta: ChannelDelta): Effect {
|
|
168
175
|
return { tag: 'ApplyChannelDelta', chan, delta };
|
|
169
176
|
},
|
|
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
177
|
sendToNick(nick: Nick, sender: ConnId, lines: RawLine[], notFoundLines?: RawLine[]): Effect {
|
|
180
178
|
const e: SendToNickEffect = { tag: 'SendToNick', nick, sender, lines };
|
|
181
179
|
if (notFoundLines !== undefined) e.notFoundLines = notFoundLines;
|
|
182
180
|
return e;
|
|
183
181
|
},
|
|
182
|
+
broadcastWallops(lines: RawLine[], except?: ConnId): Effect {
|
|
183
|
+
const e: BroadcastWallopsEffect = { tag: 'BroadcastWallops', lines };
|
|
184
|
+
if (except !== undefined) e.except = except;
|
|
185
|
+
return e;
|
|
186
|
+
},
|
|
184
187
|
} as const;
|
|
@@ -10,9 +10,12 @@ 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';
|
|
15
16
|
export * from './case-fold.js';
|
|
17
|
+
export * from './ws-subprotocol.js';
|
|
18
|
+
export * from './ws-framing.js';
|
|
16
19
|
export * from './admission.js';
|
|
17
20
|
export * from './config.js';
|
|
18
21
|
export type {
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { caseFold } from './case-fold.js';
|
|
10
|
+
import type { ChannelState } from './state/channel.js';
|
|
11
|
+
import type { ConnId, ConnectionState } from './state/connection.js';
|
|
10
12
|
|
|
11
13
|
/** Read-only wall clock, epoch milliseconds. */
|
|
12
14
|
export interface Clock {
|
|
@@ -57,7 +59,7 @@ export interface IdFactory {
|
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
61
|
* Production id factory. Generates UUIDv4-shaped strings without relying
|
|
60
|
-
* on `crypto` so the same code runs on Node
|
|
62
|
+
* on `crypto` so the same code runs on Node 24, Cloudflare Workers, and
|
|
61
63
|
* Lambda without per-platform type wrangling. Uniqueness is statistical;
|
|
62
64
|
* for cryptographic IDs, adapters can supply their own {@link IdFactory}.
|
|
63
65
|
*/
|
|
@@ -171,7 +173,8 @@ export const EmptyMotdProvider: MotdProvider = new StaticMotdProvider([]);
|
|
|
171
173
|
*/
|
|
172
174
|
export type SaslPayload =
|
|
173
175
|
| { kind: 'PLAIN'; username: string; password: string }
|
|
174
|
-
| { kind: 'RAW'; data: string }
|
|
176
|
+
| { kind: 'RAW'; data: string }
|
|
177
|
+
| { kind: 'EXTERNAL'; identity: string };
|
|
175
178
|
|
|
176
179
|
/**
|
|
177
180
|
* Outcome of an {@link AccountStore.verify} call: either the canonical
|
|
@@ -202,6 +205,12 @@ export interface AccountStore {
|
|
|
202
205
|
export interface SaslAccountCredential {
|
|
203
206
|
username: string;
|
|
204
207
|
password: string;
|
|
208
|
+
/**
|
|
209
|
+
* Optional mTLS client-certificate subject (e.g. `CN=alice` or a
|
|
210
|
+
* fingerprint hash). When set, SASL `EXTERNAL` authentications that
|
|
211
|
+
* present this subject are mapped to {@link username} as the account.
|
|
212
|
+
*/
|
|
213
|
+
certSubject?: string;
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
/**
|
|
@@ -221,23 +230,37 @@ export interface SaslAccountCredential {
|
|
|
221
230
|
*/
|
|
222
231
|
export class InMemoryAccountStore implements AccountStore {
|
|
223
232
|
private readonly creds: ReadonlyMap<string, string>;
|
|
233
|
+
private readonly certMap: ReadonlyMap<string, string>;
|
|
224
234
|
|
|
225
235
|
constructor(credentials: ReadonlyArray<SaslAccountCredential>) {
|
|
226
236
|
this.creds = new Map(credentials.map((c) => [c.username, c.password]));
|
|
237
|
+
this.certMap = new Map(
|
|
238
|
+
credentials
|
|
239
|
+
.filter((c) => c.certSubject !== undefined)
|
|
240
|
+
.map((c) => [c.certSubject as string, c.username]),
|
|
241
|
+
);
|
|
227
242
|
}
|
|
228
243
|
|
|
229
244
|
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
const m = mech.toUpperCase();
|
|
246
|
+
if (m === 'PLAIN' && payload.kind === 'PLAIN') {
|
|
247
|
+
const expected = this.creds.get(payload.username);
|
|
248
|
+
if (expected === undefined) {
|
|
249
|
+
return { ok: false, reason: 'invalid credentials' };
|
|
250
|
+
}
|
|
251
|
+
if (!constantTimeEquals(payload.password, expected)) {
|
|
252
|
+
return { ok: false, reason: 'invalid credentials' };
|
|
253
|
+
}
|
|
254
|
+
return { ok: true, account: payload.username };
|
|
236
255
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
256
|
+
if (m === 'EXTERNAL' && payload.kind === 'EXTERNAL') {
|
|
257
|
+
const account = this.certMap.get(payload.identity);
|
|
258
|
+
if (account === undefined) {
|
|
259
|
+
return { ok: false, reason: 'untrusted certificate' };
|
|
260
|
+
}
|
|
261
|
+
return { ok: true, account };
|
|
239
262
|
}
|
|
240
|
-
return { ok:
|
|
263
|
+
return { ok: false, reason: `unsupported mechanism: ${mech}` };
|
|
241
264
|
}
|
|
242
265
|
}
|
|
243
266
|
|
|
@@ -256,6 +279,52 @@ function constantTimeEquals(a: string, b: string): boolean {
|
|
|
256
279
|
return diff === 0;
|
|
257
280
|
}
|
|
258
281
|
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// mTLS identity (SASL EXTERNAL)
|
|
284
|
+
// ============================================================================
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Port that resolves the verified client-certificate identity for a
|
|
288
|
+
* connection.
|
|
289
|
+
*
|
|
290
|
+
* At the edge (CF API Shield mTLS, AWS API Gateway custom-domain mTLS) the
|
|
291
|
+
* platform terminates TLS, validates the client certificate against the
|
|
292
|
+
* configured trust store, and surfaces the certificate subject to the
|
|
293
|
+
* Worker/Lambda. The adapter captures this at connection-admission time
|
|
294
|
+
* and stores it keyed by connection ID.
|
|
295
|
+
*
|
|
296
|
+
* The method is **synchronous** — adapters pre-populate the identity at
|
|
297
|
+
* admission time (before the first reducer runs), so the SASL EXTERNAL
|
|
298
|
+
* reducer can look it up without awaiting. When no client certificate was
|
|
299
|
+
* presented (or mTLS is not configured), `getIdentity` returns `undefined`
|
|
300
|
+
* and EXTERNAL authentication fails with `904`.
|
|
301
|
+
*/
|
|
302
|
+
export interface MtlsIdentityProvider {
|
|
303
|
+
/**
|
|
304
|
+
* @returns the verified client-cert subject for `connId`, or `undefined`
|
|
305
|
+
* when no trusted certificate was presented.
|
|
306
|
+
*/
|
|
307
|
+
getIdentity(connId: string): string | undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Reference in-memory {@link MtlsIdentityProvider} backed by a static map
|
|
312
|
+
* of connection IDs to certificate subjects. Useful for tests and the
|
|
313
|
+
* local-cli adapter; cloud adapters replace this with a lookup keyed by
|
|
314
|
+
* the platform's connection identifier.
|
|
315
|
+
*/
|
|
316
|
+
export class InMemoryMtlsIdentityProvider implements MtlsIdentityProvider {
|
|
317
|
+
private readonly identities: ReadonlyMap<string, string>;
|
|
318
|
+
|
|
319
|
+
constructor(entries: ReadonlyArray<{ connId: string; identity: string }>) {
|
|
320
|
+
this.identities = new Map(entries.map((e) => [e.connId, e.identity]));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
getIdentity(connId: string): string | undefined {
|
|
324
|
+
return this.identities.get(connId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
259
328
|
// ============================================================================
|
|
260
329
|
// Chat-history persistence (IRCv3 draft/chathistory)
|
|
261
330
|
// ============================================================================
|
|
@@ -501,6 +570,285 @@ function tail<T>(arr: readonly T[], n: number): T[] {
|
|
|
501
570
|
return arr.slice(start);
|
|
502
571
|
}
|
|
503
572
|
|
|
573
|
+
// ============================================================================
|
|
574
|
+
// Nick history (WHOWAS)
|
|
575
|
+
// ============================================================================
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* One persisted sign-off record for a nick, written by the QUIT and
|
|
579
|
+
* post-registration NICK-change reducers and replayed by `WHOWAS`.
|
|
580
|
+
*
|
|
581
|
+
* `nick` preserves the original case the client used; `connId` is the
|
|
582
|
+
* connection that owned the nick at sign-off. `username` / `hostname` /
|
|
583
|
+
* `realname` mirror the {@link ConnectionState} fields at sign-off time
|
|
584
|
+
* (optional because a never-fully-registered connection may still quit with
|
|
585
|
+
* a nick). `signoffTime` is epoch milliseconds from the injected
|
|
586
|
+
* {@link Clock}.
|
|
587
|
+
*/
|
|
588
|
+
export interface NickHistoryEntry {
|
|
589
|
+
nick: string;
|
|
590
|
+
connId: string;
|
|
591
|
+
username?: string;
|
|
592
|
+
hostname?: string;
|
|
593
|
+
realname?: string;
|
|
594
|
+
signoffTime: number;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Persistence port for the IRC `WHOWAS` command.
|
|
599
|
+
*
|
|
600
|
+
* Mirrors the {@link MessageStore} pattern: the port is **synchronous**
|
|
601
|
+
* (reducers must stay pure and free of IO) and injected via {@link Ctx}.
|
|
602
|
+
* Adapters that need async storage (D1, DynamoDB) pre-load the relevant
|
|
603
|
+
* slice into a synchronously-readable store; Phase 8 ships the in-memory
|
|
604
|
+
* reference implementation only.
|
|
605
|
+
*
|
|
606
|
+
* `query` always returns matches most-recent-first (the order `WHOWAS`
|
|
607
|
+
* emits `314 RPL_WHOWASUSER` lines in), capped at `count`.
|
|
608
|
+
*/
|
|
609
|
+
export interface NickHistoryStore {
|
|
610
|
+
/** Records a sign-off entry for a nick (TTL eviction is impl-defined). */
|
|
611
|
+
record(entry: NickHistoryEntry): void;
|
|
612
|
+
/** Returns up to `count` most-recent entries for `nick` (case-insensitive). */
|
|
613
|
+
query(nick: string, count: number): NickHistoryEntry[];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Default retention window for nick-history entries: 24 hours. */
|
|
617
|
+
const DEFAULT_NICK_HISTORY_MAX_AGE = 24 * 60 * 60 * 1000;
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Reference in-memory {@link NickHistoryStore} backed by a per-nick
|
|
621
|
+
* chronological array with a configurable TTL.
|
|
622
|
+
*
|
|
623
|
+
* Used by the local CLI and by tests. Each nick keeps its sign-off records
|
|
624
|
+
* in arrival order; entries older than `maxAge` (measured against the
|
|
625
|
+
* injected {@link Clock}) are dropped lazily on `record` and `query` so the
|
|
626
|
+
* per-nick buffers do not grow unbounded.
|
|
627
|
+
*
|
|
628
|
+
* Nicks are keyed via {@link caseFold} (`rfc1459`) so lookups are
|
|
629
|
+
* case-insensitive, matching the IRC nick-comparison semantics advertised
|
|
630
|
+
* via `005 RPL_ISUPPORT CASEMAPPING=rfc1459`.
|
|
631
|
+
*/
|
|
632
|
+
export class InMemoryNickHistoryStore implements NickHistoryStore {
|
|
633
|
+
private readonly byNick = new Map<string, NickHistoryEntry[]>();
|
|
634
|
+
private readonly clock: Clock;
|
|
635
|
+
private readonly maxAge: number;
|
|
636
|
+
|
|
637
|
+
constructor(clock: Clock, opts?: { maxAge?: number }) {
|
|
638
|
+
this.clock = clock;
|
|
639
|
+
this.maxAge = opts?.maxAge ?? DEFAULT_NICK_HISTORY_MAX_AGE;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
record(entry: NickHistoryEntry): void {
|
|
643
|
+
const key = caseFold('rfc1459', entry.nick);
|
|
644
|
+
let arr = this.byNick.get(key);
|
|
645
|
+
if (arr === undefined) {
|
|
646
|
+
arr = [];
|
|
647
|
+
this.byNick.set(key, arr);
|
|
648
|
+
}
|
|
649
|
+
this.purgeExpired(arr);
|
|
650
|
+
arr.push(entry);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
query(nick: string, count: number): NickHistoryEntry[] {
|
|
654
|
+
if (count <= 0) return [];
|
|
655
|
+
const arr = this.byNick.get(caseFold('rfc1459', nick));
|
|
656
|
+
if (arr === undefined) return [];
|
|
657
|
+
this.purgeExpired(arr);
|
|
658
|
+
if (arr.length === 0) return [];
|
|
659
|
+
return arr.slice(Math.max(0, arr.length - count)).reverse();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Drops entries whose `signoffTime` is older than `now - maxAge`, in place.
|
|
664
|
+
* Entries arrive in chronological order (append-only), so the first alive
|
|
665
|
+
* entry marks the boundary; everything before it is expired.
|
|
666
|
+
*/
|
|
667
|
+
private purgeExpired(arr: NickHistoryEntry[]): void {
|
|
668
|
+
const cutoff = this.clock.now() - this.maxAge;
|
|
669
|
+
let firstAlive = arr.findIndex((e) => e.signoffTime >= cutoff);
|
|
670
|
+
if (firstAlive === -1) firstAlive = arr.length;
|
|
671
|
+
arr.splice(0, firstAlive);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ============================================================================
|
|
676
|
+
// Network statistics (LUSERS / STATS)
|
|
677
|
+
// ============================================================================
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Snapshot of network-wide counts returned by a {@link ServerStats} backend.
|
|
681
|
+
*
|
|
682
|
+
* The shape mirrors the fields the `LUSERS` and `STATS` reducers need to
|
|
683
|
+
* format their numerics:
|
|
684
|
+
* - `users` / `invisible`: the split registered-user counts that drive
|
|
685
|
+
* `251 RPL_LUSERCLIENT` ("There are X users and Y invisible on Z
|
|
686
|
+
* servers"). `users` is the *visible* (non-`+i`) registered count;
|
|
687
|
+
* `invisible` is the `+i` registered count. Total registered users
|
|
688
|
+
* is `users + invisible`.
|
|
689
|
+
* - `opers`: count of IRC operators; emitted as `252 RPL_LUSEROP` when
|
|
690
|
+
* non-zero. An oper counts here regardless of `+i` (an `+o` oper who
|
|
691
|
+
* is also `+i` still bumps `opers`, `invisible`, and `users` is
|
|
692
|
+
* computed as visible = total-registered − invisible).
|
|
693
|
+
* - `unknownConnections`: pre-registration connection count; emitted as
|
|
694
|
+
* `253 RPL_LUSERUNKNOWN` when non-zero.
|
|
695
|
+
* - `channels`: live channel count; emitted as `254 RPL_LUSERCHANNELS`
|
|
696
|
+
* when non-zero.
|
|
697
|
+
* - `servers`: server count seen by `251`/`255`. Always 1 for the
|
|
698
|
+
* single-server serverless deployment; S2S aggregation is a PLAN
|
|
699
|
+
* non-goal.
|
|
700
|
+
* - `localConns` / `globalConns`: total connection counts (registered
|
|
701
|
+
* + pre-registration) for the optional `265 RPL_LOCALUSERS` /
|
|
702
|
+
* `266 RPL_GLOBALUSERS` numerics. Equal for a single-server network.
|
|
703
|
+
* - `maxLocalConns` / `maxGlobalConns`: high-water marks accompanying
|
|
704
|
+
* `265`/`266`. When the backend has no recorded max it reports the
|
|
705
|
+
* current count (so the wire numeric never claims a max below live).
|
|
706
|
+
* - `uptimeStartedAt`: epoch-ms the server (or stats backend) started
|
|
707
|
+
* counting from. Drives the `STATS u` uptime line.
|
|
708
|
+
*/
|
|
709
|
+
export interface ServerStatsSnapshot {
|
|
710
|
+
users: number;
|
|
711
|
+
invisible: number;
|
|
712
|
+
opers: number;
|
|
713
|
+
unknownConnections: number;
|
|
714
|
+
channels: number;
|
|
715
|
+
servers: number;
|
|
716
|
+
localConns: number;
|
|
717
|
+
globalConns: number;
|
|
718
|
+
maxLocalConns: number;
|
|
719
|
+
maxGlobalConns: number;
|
|
720
|
+
uptimeStartedAt: number;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Port that aggregates network-wide statistics for `LUSERS` and `STATS`.
|
|
725
|
+
*
|
|
726
|
+
* Unlike the synchronous {@link MotdProvider} / {@link AccountStore} ports
|
|
727
|
+
* (which reducers consume directly via {@link Ctx}), this port is
|
|
728
|
+
* **asynchronous** (`Promise`-returning) because the aggregation crosses
|
|
729
|
+
* every connection and channel in the deployment — it cannot be pre-loaded
|
|
730
|
+
* into a synchronous view the way MOTD or a credential table can. The
|
|
731
|
+
* actor layer awaits {@link getStats} once per `LUSERS` / `STATS`
|
|
732
|
+
* invocation, then passes the resulting {@link ServerStatsSnapshot} to the
|
|
733
|
+
* pure reducers (`lusersReducer`, `statsReducer`). This mirrors the
|
|
734
|
+
* `listChannels` → `listReducer` pattern.
|
|
735
|
+
*
|
|
736
|
+
* Each adapter implements this against its authoritative store:
|
|
737
|
+
* - in-memory runtime: derive from its `Map`s (reference impl
|
|
738
|
+
* {@link InMemoryStats}).
|
|
739
|
+
* - CF: aggregate via `listChannels` / connection-enumeration RPCs.
|
|
740
|
+
* - AWS: `Scan` / `Count` queries against `Connections` / `ChannelMeta`.
|
|
741
|
+
*/
|
|
742
|
+
export interface ServerStats {
|
|
743
|
+
getStats(): Promise<ServerStatsSnapshot>;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Aggregates a snapshot from already-collected connection state and a
|
|
748
|
+
* channel count.
|
|
749
|
+
*
|
|
750
|
+
* Pure: backends that gather their data through async I/O (CF RPC fan-out,
|
|
751
|
+
* AWS DynamoDB `Scan`) call this once they have the raw inputs. The
|
|
752
|
+
* {@link InMemoryStats} reference backend delegates here so the
|
|
753
|
+
* classification logic (oper / invisible / unknown) lives in exactly one
|
|
754
|
+
* place and is unit-tested independently of any adapter's I/O.
|
|
755
|
+
*
|
|
756
|
+
* Counting rules (RFC 2812 §4.6.2 + modern practice):
|
|
757
|
+
* - `users` is the **visible** registered count (total registered minus
|
|
758
|
+
* `+i`); the first number in "There are X users and Y invisible".
|
|
759
|
+
* - `invisible` is the `+i` registered count; the second number.
|
|
760
|
+
* - An oper who is also `+i` increments `opers`, `invisible`, and
|
|
761
|
+
* reduces `users` (because they are invisible) — matching charybdis.
|
|
762
|
+
* - `localConns` is every tracked connection (registered + unknown).
|
|
763
|
+
*/
|
|
764
|
+
export function computeStatsSnapshot(
|
|
765
|
+
connections: Iterable<ConnectionState>,
|
|
766
|
+
channelCount: number,
|
|
767
|
+
uptimeStartedAt: number,
|
|
768
|
+
opts?: { maxLocalConns?: number; maxGlobalConns?: number; servers?: number },
|
|
769
|
+
): ServerStatsSnapshot {
|
|
770
|
+
let registered = 0;
|
|
771
|
+
let invisible = 0;
|
|
772
|
+
let opers = 0;
|
|
773
|
+
let unknown = 0;
|
|
774
|
+
let total = 0;
|
|
775
|
+
for (const conn of connections) {
|
|
776
|
+
total++;
|
|
777
|
+
if (conn.registration !== 'registered') {
|
|
778
|
+
unknown++;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
registered++;
|
|
782
|
+
if (conn.userModes.invisible) invisible++;
|
|
783
|
+
if (conn.userModes.oper) opers++;
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
users: registered - invisible,
|
|
787
|
+
invisible,
|
|
788
|
+
opers,
|
|
789
|
+
unknownConnections: unknown,
|
|
790
|
+
channels: channelCount,
|
|
791
|
+
servers: opts?.servers ?? 1,
|
|
792
|
+
localConns: total,
|
|
793
|
+
globalConns: total,
|
|
794
|
+
// The wire numeric must never claim a max below the current live count;
|
|
795
|
+
// when the backend's recorded max is stale (or never seeded) the live
|
|
796
|
+
// count is the lower bound.
|
|
797
|
+
maxLocalConns: Math.max(opts?.maxLocalConns ?? 0, total),
|
|
798
|
+
maxGlobalConns: Math.max(opts?.maxGlobalConns ?? 0, total),
|
|
799
|
+
uptimeStartedAt,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Reference in-memory {@link ServerStats} backed by the live `Map`s the
|
|
805
|
+
* {@link InMemoryRuntime} (and tests) already maintain.
|
|
806
|
+
*
|
|
807
|
+
* The constructor takes *references* to the runtime's mutable maps; later
|
|
808
|
+
* mutations are visible on the next {@link getStats} call without
|
|
809
|
+
* re-binding. Used by the local CLI, by integration tests, and as the
|
|
810
|
+
* contract spec the CF / AWS backends mirror.
|
|
811
|
+
*
|
|
812
|
+
* `uptimeStartedAt` is supplied at construction (typically the runtime's
|
|
813
|
+
* boot time). `maxLocalConns` / `maxGlobalConns` are optional high-water
|
|
814
|
+
* marks; when omitted the live count is reported as the max so `265`/`266`
|
|
815
|
+
* never regress below the current connection total.
|
|
816
|
+
*/
|
|
817
|
+
export class InMemoryStats implements ServerStats {
|
|
818
|
+
private readonly connections: ReadonlyMap<ConnId, ConnectionState>;
|
|
819
|
+
private readonly channels: ReadonlyMap<string, ChannelState>;
|
|
820
|
+
private readonly uptimeStartedAt: number;
|
|
821
|
+
private readonly maxLocalConns: number | undefined;
|
|
822
|
+
private readonly maxGlobalConns: number | undefined;
|
|
823
|
+
|
|
824
|
+
constructor(opts: {
|
|
825
|
+
connections: ReadonlyMap<ConnId, ConnectionState>;
|
|
826
|
+
channels: ReadonlyMap<string, ChannelState>;
|
|
827
|
+
uptimeStartedAt: number;
|
|
828
|
+
clock?: Clock;
|
|
829
|
+
maxLocalConns?: number;
|
|
830
|
+
maxGlobalConns?: number;
|
|
831
|
+
}) {
|
|
832
|
+
this.connections = opts.connections;
|
|
833
|
+
this.channels = opts.channels;
|
|
834
|
+
this.uptimeStartedAt = opts.uptimeStartedAt;
|
|
835
|
+
this.maxLocalConns = opts.maxLocalConns;
|
|
836
|
+
this.maxGlobalConns = opts.maxGlobalConns;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async getStats(): Promise<ServerStatsSnapshot> {
|
|
840
|
+
return computeStatsSnapshot(
|
|
841
|
+
this.connections.values(),
|
|
842
|
+
this.channels.size,
|
|
843
|
+
this.uptimeStartedAt,
|
|
844
|
+
{
|
|
845
|
+
...(this.maxLocalConns !== undefined ? { maxLocalConns: this.maxLocalConns } : {}),
|
|
846
|
+
...(this.maxGlobalConns !== undefined ? { maxGlobalConns: this.maxGlobalConns } : {}),
|
|
847
|
+
},
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
504
852
|
// ============================================================================
|
|
505
853
|
// Observability — structured logger
|
|
506
854
|
// ============================================================================
|
|
@@ -854,7 +1202,7 @@ export class ConsoleLogger implements Logger {
|
|
|
854
1202
|
|
|
855
1203
|
/** Returns the global `console` as a {@link ConsoleLoggerSinks}. */
|
|
856
1204
|
function globalThisConsoleSinks(): ConsoleLoggerSinks {
|
|
857
|
-
// `globalThis.console` is universal across Node
|
|
1205
|
+
// `globalThis.console` is universal across Node 24, Workers, and Lambda.
|
|
858
1206
|
// Reference it lazily so tests that stub the logger do not need to
|
|
859
1207
|
// replace the global.
|
|
860
1208
|
const c = (globalThis as { console?: GlobalConsole }).console;
|
|
@@ -13,9 +13,39 @@ 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
|
+
// LUSERS (RFC 2812 §4.6.2). Network-wide user/channel/server counts.
|
|
20
|
+
// 251 is the human-readable summary; 252–254 are emitted only when their
|
|
21
|
+
// count is non-zero (per RFC). 255 is the local-server summary. 265/266
|
|
22
|
+
// carry the optional local/global max-connection context.
|
|
23
|
+
RPL_LUSERCLIENT: 251,
|
|
24
|
+
RPL_LUSEROP: 252,
|
|
25
|
+
RPL_LUSERUNKNOWN: 253,
|
|
26
|
+
RPL_LUSERCHANNELS: 254,
|
|
27
|
+
RPL_LUSERME: 255,
|
|
28
|
+
RPL_LOCALUSERS: 265,
|
|
29
|
+
RPL_GLOBALUSERS: 266,
|
|
30
|
+
|
|
31
|
+
// STATS (RFC 2812 §4.6.3 / §5.1). 211 is link-info (the `l` query),
|
|
32
|
+
// 242 is the uptime line (the `u` query), and 219 terminates every
|
|
33
|
+
// STATS reply regardless of query letter. The remaining RPL_STATS*
|
|
34
|
+
// codes (212–218, 240–248) are deferred behind per-letter tests.
|
|
35
|
+
RPL_STATSLINKINFO: 211,
|
|
36
|
+
RPL_ENDOFSTATS: 219,
|
|
37
|
+
RPL_STATSUPTIME: 242,
|
|
38
|
+
|
|
39
|
+
// TRACE (RFC 2812 §4.6.3). In a single-server deployment TRACE collapses
|
|
40
|
+
// to a local-server line (200/206) plus optional per-connection detail
|
|
41
|
+
// (204 oper / 205 user), terminated by 262. Multi-hop S2S tracing is a
|
|
42
|
+
// PLAN non-goal.
|
|
43
|
+
RPL_TRACELINK: 200,
|
|
44
|
+
RPL_TRACEOPERATOR: 204,
|
|
45
|
+
RPL_TRACEUSER: 205,
|
|
46
|
+
RPL_TRACESERVER: 206,
|
|
47
|
+
RPL_ENDOFTRACE: 262,
|
|
48
|
+
|
|
19
49
|
// Away / userhost / ison
|
|
20
50
|
RPL_AWAY: 301,
|
|
21
51
|
RPL_USERHOST: 302,
|
|
@@ -23,7 +53,10 @@ export const Numerics = {
|
|
|
23
53
|
RPL_UNAWAY: 305,
|
|
24
54
|
RPL_NOWAWAY: 306,
|
|
25
55
|
|
|
26
|
-
//
|
|
56
|
+
// Version
|
|
57
|
+
RPL_VERSION: 351,
|
|
58
|
+
|
|
59
|
+
// WHOIS / WHOWAS
|
|
27
60
|
RPL_WHOISUSER: 311,
|
|
28
61
|
RPL_WHOISSERVER: 312,
|
|
29
62
|
RPL_WHOISOPERATOR: 313,
|
|
@@ -31,6 +64,7 @@ export const Numerics = {
|
|
|
31
64
|
RPL_ENDOFWHOIS: 318,
|
|
32
65
|
RPL_WHOISCHANNELS: 319,
|
|
33
66
|
RPL_WHOISACCOUNT: 330,
|
|
67
|
+
RPL_WHOWASUSER: 314,
|
|
34
68
|
|
|
35
69
|
// LIST
|
|
36
70
|
RPL_LISTSTART: 321,
|
|
@@ -57,9 +91,8 @@ export const Numerics = {
|
|
|
57
91
|
RPL_NAMREPLY: 353,
|
|
58
92
|
RPL_ENDOFNAMES: 366,
|
|
59
93
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
RPL_ENDOFLINKS: 365,
|
|
94
|
+
// Ban / whowas. (The S2S `LINKS` verb and its `RPL_LINKS`/`RPL_ENDOFLINKS`
|
|
95
|
+
// numerics were dropped — S2S is a PLAN non-goal.)
|
|
63
96
|
RPL_BANLIST: 367,
|
|
64
97
|
RPL_ENDOFBANLIST: 368,
|
|
65
98
|
RPL_ENDOFWHOWAS: 369,
|
|
@@ -71,10 +104,12 @@ export const Numerics = {
|
|
|
71
104
|
RPL_MOTDSTART: 375,
|
|
72
105
|
RPL_ENDOFMOTD: 376,
|
|
73
106
|
|
|
74
|
-
// Oper
|
|
107
|
+
// Oper. RPL_REHASHING is emitted by the oper-gated `REHASH` reducer when a
|
|
108
|
+
// config reload is requested (the actor re-invokes the adapter's loader).
|
|
109
|
+
// (The RFC 2812 `SERVICE` verb and its `RPL_YOURESERVICE` numeric were
|
|
110
|
+
// dropped — obsolete, never widely implemented.)
|
|
75
111
|
RPL_YOUREOPER: 381,
|
|
76
112
|
RPL_REHASHING: 382,
|
|
77
|
-
RPL_YOURESERVICE: 383,
|
|
78
113
|
RPL_TIME: 391,
|
|
79
114
|
|
|
80
115
|
// SASL (IRCv3 sasl-3.2)
|
|
@@ -96,7 +131,8 @@ export const Numerics = {
|
|
|
96
131
|
ERR_TOOMANYCHANNELS: 405,
|
|
97
132
|
ERR_WASNOSUCHNICK: 406,
|
|
98
133
|
ERR_TOOMANYTARGETS: 407,
|
|
99
|
-
ERR_NOSUCHSERVICE
|
|
134
|
+
// `ERR_NOSUCHSERVICE` (408) was dropped with the obsolete RFC 2812 `SERVICE`
|
|
135
|
+
// verb.
|
|
100
136
|
ERR_INVALIDCAPCMD: 410,
|
|
101
137
|
ERR_NORECIPIENT: 411,
|
|
102
138
|
ERR_NOTEXTTOSEND: 412,
|
|
@@ -115,8 +151,8 @@ export const Numerics = {
|
|
|
115
151
|
ERR_NOTONCHANNEL: 442,
|
|
116
152
|
ERR_USERONCHANNEL: 443,
|
|
117
153
|
ERR_NOLOGIN: 444,
|
|
118
|
-
ERR_SUMMONDISABLED
|
|
119
|
-
|
|
154
|
+
// `ERR_SUMMONDISABLED` (445) / `ERR_USERSDISABLED` (446) were dropped with
|
|
155
|
+
// the obsolete RFC 2812 `SUMMON`/`USERS` verbs.
|
|
120
156
|
ERR_NOTREGISTERED: 451,
|
|
121
157
|
ERR_NEEDMOREPARAMS: 461,
|
|
122
158
|
ERR_ALREADYREGISTRED: 462,
|
|
@@ -134,7 +170,8 @@ export const Numerics = {
|
|
|
134
170
|
ERR_BANLISTFULL: 478,
|
|
135
171
|
ERR_NOPRIVILEGES: 481,
|
|
136
172
|
ERR_CHANOPRIVSNEEDED: 482,
|
|
137
|
-
ERR_CANTKILLSERVER
|
|
173
|
+
// `ERR_CANTKILLSERVER` (483) was dropped with the S2S `SQUIT` verb — S2S is
|
|
174
|
+
// a PLAN non-goal.
|
|
138
175
|
ERR_NOOPERHOST: 491,
|
|
139
176
|
ERR_UMODEUNKNOWNFLAG: 501,
|
|
140
177
|
ERR_USERSDONTMATCH: 502,
|