serverless-ircd 0.4.0 → 0.6.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 +34 -2
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/CHANGELOG.md +435 -0
- package/README.md +206 -27
- package/apps/aws-stack/README.md +37 -3
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +106 -13
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +49 -3
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.toml +5 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +138 -28
- package/apps/local-cli/tests/e2e.test.ts +113 -29
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +262 -0
- package/biome.json +1 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +15 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +69 -0
- package/packages/aws-adapter/src/handlers/connect.ts +36 -5
- package/packages/aws-adapter/src/handlers/default.ts +66 -5
- package/packages/aws-adapter/src/handlers/index.ts +41 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +18 -0
- package/packages/aws-adapter/src/index.ts +2 -0
- package/packages/aws-adapter/src/serialize.ts +40 -2
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +140 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +100 -4
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +238 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +62 -8
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/cf-adapter/package.json +6 -1
- package/packages/cf-adapter/src/cf-runtime.ts +66 -1
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/connection-do.ts +185 -54
- package/packages/cf-adapter/src/env.ts +25 -6
- package/packages/cf-adapter/src/index.ts +2 -0
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/serialize.ts +25 -4
- 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 +1 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +4 -4
- package/packages/cf-adapter/tests/cf-runtime.test.ts +307 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +119 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -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 +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +108 -4
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- 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 +7 -7
- 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/vitest.config.ts +1 -1
- package/packages/cf-adapter/wrangler.test.toml +7 -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 +1 -1
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +38 -4
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +42 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/isupport.ts +59 -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 +12 -9
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- 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/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- 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/tagmsg.ts +71 -1
- 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/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +72 -10
- package/packages/irc-core/src/effects.ts +41 -1
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +568 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +57 -11
- package/packages/irc-core/src/protocol/outbound.ts +36 -4
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +120 -1
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -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 +129 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- 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 +357 -0
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +90 -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/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- 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-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +699 -19
- package/packages/irc-server/src/dispatch.ts +109 -16
- package/packages/irc-server/src/routing.ts +3 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +10 -7
- package/packages/irc-server/tests/actor.test.ts +2523 -42
- package/packages/irc-server/tests/dispatch.test.ts +300 -2
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +1 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +9 -1
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/package.json +6 -1
- package/tools/seed-cf-accounts.ts +4 -1
- 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
|
@@ -33,13 +33,17 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import {
|
|
36
|
+
DEFAULT_MULTILINE_MAX_BYTES,
|
|
37
|
+
MULTILINE_CAP_NAME,
|
|
36
38
|
SASL_CAP_NAME,
|
|
37
39
|
SUPPORTED_CAPABILITIES,
|
|
38
40
|
capToLsString,
|
|
39
41
|
isSupportedCap,
|
|
42
|
+
multilineCapValue,
|
|
40
43
|
saslCapValue,
|
|
41
44
|
splitCapsForWire,
|
|
42
45
|
} from '../caps/capabilities.js';
|
|
46
|
+
import { STS_CAP_NAME, stsCapValue } from '../caps/sts.js';
|
|
43
47
|
import { Effect } from '../effects.js';
|
|
44
48
|
import type { RawLine } from '../effects.js';
|
|
45
49
|
import { Numerics } from '../protocol/numerics.js';
|
|
@@ -91,11 +95,21 @@ function handleLs(state: ConnectionState, ctx: Ctx): ReducerResult<ConnectionSta
|
|
|
91
95
|
|
|
92
96
|
const target = state.nick ?? '*';
|
|
93
97
|
const hasMtls = ctx.mtlsIdentity !== undefined;
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
const multilineMax = ctx.serverConfig.multilineMaxBytes ?? DEFAULT_MULTILINE_MAX_BYTES;
|
|
99
|
+
const tokens = SUPPORTED_CAPABILITIES.map((cap) => {
|
|
100
|
+
if (cap.name === SASL_CAP_NAME) {
|
|
101
|
+
return capToLsString({ ...cap, value: saslCapValue(hasMtls) });
|
|
102
|
+
}
|
|
103
|
+
if (cap.name === MULTILINE_CAP_NAME) {
|
|
104
|
+
return capToLsString({ ...cap, value: multilineCapValue(multilineMax) });
|
|
105
|
+
}
|
|
106
|
+
return capToLsString(cap);
|
|
107
|
+
});
|
|
108
|
+
// STS is conditionally advertised: appended only when the deployment
|
|
109
|
+
// supplies a policy, and the value is per-transport (plaintext sees the
|
|
110
|
+
// full `duration,port[,preload]`; TLS sees only `duration`).
|
|
111
|
+
const stsValue = stsCapValue(ctx.serverConfig.sts, ctx.connection.secure);
|
|
112
|
+
if (stsValue !== undefined) tokens.push(`${STS_CAP_NAME}=${stsValue}`);
|
|
99
113
|
const maxLen = maxCapsContentLen(ctx, target);
|
|
100
114
|
const chunks = splitCapsForWire(tokens, maxLen);
|
|
101
115
|
|
|
@@ -164,8 +178,12 @@ function handleReq(
|
|
|
164
178
|
const requested = rawCaps.split(' ').filter((c) => c.length > 0);
|
|
165
179
|
const target = state.nick ?? '*';
|
|
166
180
|
|
|
181
|
+
// STS is conditionally supported: ACKed only when the deployment supplies
|
|
182
|
+
// a policy. Other caps consult the static table.
|
|
183
|
+
const stsConfigured = ctx.serverConfig.sts !== undefined;
|
|
167
184
|
const allSupported = requested.every((tok) => {
|
|
168
185
|
const name = tok.startsWith('-') ? tok.slice(1) : tok;
|
|
186
|
+
if (name === STS_CAP_NAME) return stsConfigured;
|
|
169
187
|
return isSupportedCap(name);
|
|
170
188
|
});
|
|
171
189
|
|
|
@@ -141,7 +141,17 @@ function handleChannelSub(
|
|
|
141
141
|
// LATEST / BEFORE / AFTER / AROUND
|
|
142
142
|
const pivot = msg.params[2];
|
|
143
143
|
const limitRaw = msg.params[3];
|
|
144
|
-
|
|
144
|
+
|
|
145
|
+
// IRCv3 draft/read-marker integration: BEFORE without an explicit pivot
|
|
146
|
+
// falls back to the connection's per-channel last-read marker so a client
|
|
147
|
+
// can resume "everything before my last read position" without re-supplying
|
|
148
|
+
// the msgid. AFTER/AROUND still require an explicit pivot.
|
|
149
|
+
const markerPivot =
|
|
150
|
+
sub === 'BEFORE' && pivot === undefined
|
|
151
|
+
? ctx.connection.lastReadMarkers?.get(chanLower)
|
|
152
|
+
: undefined;
|
|
153
|
+
|
|
154
|
+
if (sub !== 'LATEST' && pivot === undefined && markerPivot === undefined) {
|
|
145
155
|
return { effects: [Effect.send(ctx.connId, [needMoreParamsLine(ctx)])] };
|
|
146
156
|
}
|
|
147
157
|
const limit = parseLimit(limitRaw);
|
|
@@ -163,12 +173,13 @@ function handleChannelSub(
|
|
|
163
173
|
return { effects: buildChathistoryBatch(ctx, channel.name, result) };
|
|
164
174
|
}
|
|
165
175
|
|
|
166
|
-
// BEFORE / AFTER / AROUND — pivot is required.
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
// BEFORE / AFTER / AROUND — pivot is required. Resolve from the explicit
|
|
177
|
+
// msgid when supplied, otherwise from the marker fallback computed above.
|
|
178
|
+
// The guard near the top already guaranteed at least one is defined.
|
|
179
|
+
const pivotMsgid = (pivot ?? markerPivot) as string;
|
|
180
|
+
// An explicit pivot must reference a retained message; a marker fallback
|
|
181
|
+
// may point at an evicted message (→ empty result, not INVALID_PARAMS).
|
|
182
|
+
if (pivot !== undefined && !storeHas(ctx, chanLower, pivotMsgid)) {
|
|
172
183
|
return { effects: [Effect.send(ctx.connId, [invalidParamsLine(ctx)])] };
|
|
173
184
|
}
|
|
174
185
|
const result = storeQuery(ctx, {
|
|
@@ -228,16 +239,34 @@ export function buildChathistoryBatch(
|
|
|
228
239
|
/**
|
|
229
240
|
* Reconstructs the wire line for one stored message, decorated with the
|
|
230
241
|
* original server-time and msgid tags. PRIVMSG/NOTICE carry the trailing
|
|
231
|
-
* text; TAGMSG has no trailing parameter.
|
|
242
|
+
* text; TAGMSG has no trailing parameter. Optional `clientTags` recorded
|
|
243
|
+
* with the message (e.g. `+draft/multiline`) are appended to the tag
|
|
244
|
+
* section so chathistory clients can recognise the message kind.
|
|
232
245
|
*/
|
|
233
246
|
export function formatReplayLine(m: StoredMessage, displayChan: string): RawLine {
|
|
234
247
|
const stamp = formatServerTime(m.time);
|
|
235
248
|
const prefix = storedHostmask(m);
|
|
236
|
-
const
|
|
249
|
+
const tags = buildReplayTags(stamp, m.msgid, m.clientTags);
|
|
237
250
|
if (m.command === 'TAGMSG') {
|
|
238
|
-
return { text: `${
|
|
251
|
+
return { text: `${tags} :${prefix} TAGMSG ${displayChan}` };
|
|
252
|
+
}
|
|
253
|
+
return { text: `${tags} :${prefix} ${m.command} ${displayChan} :${m.text}` };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Builds the `@time=…;msgid=…[;+client-tag…]` tag section for a replay line.
|
|
258
|
+
* Client tags (when present) are appended after the server tags, matching the
|
|
259
|
+
* IRCv3 message-tags wire order.
|
|
260
|
+
*/
|
|
261
|
+
function buildReplayTags(
|
|
262
|
+
stamp: string,
|
|
263
|
+
msgid: string,
|
|
264
|
+
clientTags: readonly string[] | undefined,
|
|
265
|
+
): string {
|
|
266
|
+
if (clientTags === undefined || clientTags.length === 0) {
|
|
267
|
+
return `@time=${stamp};msgid=${msgid}`;
|
|
239
268
|
}
|
|
240
|
-
return {
|
|
269
|
+
return `@time=${stamp};msgid=${msgid};${clientTags.join(';')}`;
|
|
241
270
|
}
|
|
242
271
|
|
|
243
272
|
/** Builds the `nick!user@host` source for a stored message. */
|
|
@@ -19,6 +19,8 @@ export { handleJoinZero, isValidChannelName, joinReducer } from './join.js';
|
|
|
19
19
|
export { partReducer } from './part.js';
|
|
20
20
|
export { motdReducer } from './motd.js';
|
|
21
21
|
export { operReducer, matchOperCred } from './oper.js';
|
|
22
|
+
export { rehashReducer, rehashLine, buildRehashTrailing } from './rehash.js';
|
|
23
|
+
export type { RehashResult } from './rehash.js';
|
|
22
24
|
export {
|
|
23
25
|
isChannelTarget,
|
|
24
26
|
noticeChannelReducer,
|
|
@@ -27,6 +29,8 @@ export {
|
|
|
27
29
|
privmsgUserReducer,
|
|
28
30
|
} from './privmsg.js';
|
|
29
31
|
export { kickReducer } from './kick.js';
|
|
32
|
+
export { killReducer, formatKillReason } from './kill.js';
|
|
33
|
+
export { wallopsReducer, wallopsLine } from './wallops.js';
|
|
30
34
|
export { topicReducer } from './topic.js';
|
|
31
35
|
export { namesReducer } from './names.js';
|
|
32
36
|
export { listReducer } from './list.js';
|
|
@@ -35,11 +39,17 @@ export { inviteReducer } from './invite.js';
|
|
|
35
39
|
export { whoReducer } from './who.js';
|
|
36
40
|
export { whoisReducer } from './whois.js';
|
|
37
41
|
export { whowasReducer } from './whowas.js';
|
|
42
|
+
export { traceReducer } from './trace.js';
|
|
43
|
+
export { lusersReducer } from './lusers.js';
|
|
44
|
+
export { statsReducer } from './stats.js';
|
|
38
45
|
export { generateIsupport } from './isupport.js';
|
|
39
46
|
export { capReducer } from './cap.js';
|
|
40
47
|
export { emitChghost } from './chghost.js';
|
|
41
48
|
export type { ChgHostInput } from './chghost.js';
|
|
49
|
+
export { emitAccountNotify } from './account-notify.js';
|
|
50
|
+
export type { AccountNotifyInput } from './account-notify.js';
|
|
42
51
|
export { awayReducer } from './away.js';
|
|
52
|
+
export { setnameReducer } from './setname.js';
|
|
43
53
|
export { authenticateReducer } from './sasl.js';
|
|
44
54
|
export {
|
|
45
55
|
chathistoryReducer,
|
|
@@ -48,6 +58,18 @@ export {
|
|
|
48
58
|
CHATHISTORY_CAP,
|
|
49
59
|
} from './chathistory.js';
|
|
50
60
|
export { tagmsgChannelReducer, tagmsgUserReducer } from './tagmsg.js';
|
|
61
|
+
export {
|
|
62
|
+
DEFAULT_MULTILINE_MAX_BYTES,
|
|
63
|
+
MULTILINE_CAP,
|
|
64
|
+
type FinalizeMultilineResult,
|
|
65
|
+
type MultilineEntry,
|
|
66
|
+
type MultilineFanout,
|
|
67
|
+
buildMultilineFanout,
|
|
68
|
+
finalizeMultilineBatch,
|
|
69
|
+
joinMultiline,
|
|
70
|
+
multilineByteLength,
|
|
71
|
+
recordMultilineMessage,
|
|
72
|
+
} from './multiline.js';
|
|
51
73
|
export {
|
|
52
74
|
versionReducer,
|
|
53
75
|
timeReducer,
|
|
@@ -56,3 +78,23 @@ export {
|
|
|
56
78
|
} from './server-info.js';
|
|
57
79
|
export { userhostReducer, formatUserhostReply } from './userhost.js';
|
|
58
80
|
export { isonReducer } from './ison.js';
|
|
81
|
+
export {
|
|
82
|
+
monitorReducer,
|
|
83
|
+
MONITOR_CAP,
|
|
84
|
+
DEFAULT_MONITOR_LIMIT,
|
|
85
|
+
} from './monitor.js';
|
|
86
|
+
export {
|
|
87
|
+
READ_MARKER_CAP,
|
|
88
|
+
READ_MARKER_TAG,
|
|
89
|
+
isReadMarkerTagmsg,
|
|
90
|
+
persistReadMarker,
|
|
91
|
+
readMarkerMsgid,
|
|
92
|
+
seedReadMarkers,
|
|
93
|
+
} from './read-marker.js';
|
|
94
|
+
export {
|
|
95
|
+
PRE_AWAY_CAP,
|
|
96
|
+
clearPersistedAway,
|
|
97
|
+
nowAwayLine,
|
|
98
|
+
persistAway,
|
|
99
|
+
replayPersistedAway,
|
|
100
|
+
} from './pre-away.js';
|
|
@@ -42,12 +42,10 @@ function callerHostmask(conn: ConnectionState): string {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Formats a numeric error/reply line addressed to the connection's nick. */
|
|
45
|
-
function numericErr(ctx: Ctx, code: number, trailing: string, middle
|
|
45
|
+
function numericErr(ctx: Ctx, code: number, trailing: string, middle: string): RawLine {
|
|
46
46
|
const nick = ctx.connection.nick ?? '*';
|
|
47
47
|
const codeStr = code.toString().padStart(3, '0');
|
|
48
|
-
const parts = [`:${ctx.serverName}`, codeStr, nick];
|
|
49
|
-
if (middle !== undefined) parts.push(middle);
|
|
50
|
-
parts.push(`:${trailing}`);
|
|
48
|
+
const parts = [`:${ctx.serverName}`, codeStr, nick, middle, `:${trailing}`];
|
|
51
49
|
return { text: parts.join(' ') };
|
|
52
50
|
}
|
|
53
51
|
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
* per-deployment (PLAN §10 capacity planning).
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { DEFAULT_MULTILINE_MAX_BYTES } from '../caps/capabilities.js';
|
|
18
19
|
import type { RawLine } from '../effects.js';
|
|
19
20
|
import type { ServerConfig } from '../types.js';
|
|
21
|
+
import { DEFAULT_MONITOR_LIMIT } from './monitor.js';
|
|
20
22
|
|
|
21
23
|
/** Max bytes per IRC wire line, including trailing `\r\n`. */
|
|
22
24
|
const LINE_MAX_BYTES = 510;
|
|
@@ -36,6 +38,32 @@ const CHANMODES = {
|
|
|
36
38
|
/** Channel-user prefix sigils, in PREFIX order (highest rank first). */
|
|
37
39
|
const PREFIX = '(ov)@+';
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* STATUSMSG delivery prefixes — the channel-user sigils a sender may prefix
|
|
43
|
+
* a target with (`@#channel` / `+#channel`) so only that rank and above
|
|
44
|
+
* receive the message. Mirrors the PREFIX sigil tail.
|
|
45
|
+
*/
|
|
46
|
+
const STATUSMSG_PREFIXES = '@+';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Default extended-ban prefix when {@link ServerConfig.extbanPrefix} is
|
|
50
|
+
* unset. `~` is the de-facto IRC standard (matches nearly every daemon).
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_EXTBAN_PREFIX = '~';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extended-ban types this server understands independent of services.
|
|
56
|
+
* `q` = quiet ban (prevents matching targets from speaking).
|
|
57
|
+
*/
|
|
58
|
+
const EXTBAN_TYPES = 'q';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Account-name extended-ban type, advertised as a dedicated `ACCOUNTEXTBAN=a`
|
|
62
|
+
* token only when services are enabled, since account bans require registered
|
|
63
|
+
* nicks to be meaningful.
|
|
64
|
+
*/
|
|
65
|
+
const ACCOUNTEXTBAN_TYPE = 'a';
|
|
66
|
+
|
|
39
67
|
/** Channel name sigils this server recognises. */
|
|
40
68
|
const CHANTYPES = '#';
|
|
41
69
|
|
|
@@ -56,6 +84,9 @@ const TRAILER = 'are supported by this server';
|
|
|
56
84
|
* can diff between servers. Tokens are space-separated, never empty.
|
|
57
85
|
*/
|
|
58
86
|
function buildTokens(cfg: ServerConfig): string[] {
|
|
87
|
+
const monitorLimit = cfg.monitorLimit ?? DEFAULT_MONITOR_LIMIT;
|
|
88
|
+
const extbanPrefix = cfg.extbanPrefix ?? DEFAULT_EXTBAN_PREFIX;
|
|
89
|
+
const multilineMax = cfg.multilineMaxBytes ?? DEFAULT_MULTILINE_MAX_BYTES;
|
|
59
90
|
return [
|
|
60
91
|
`CHANTYPES=${CHANTYPES}`,
|
|
61
92
|
`CHANMODES=${CHANMODES.list},${CHANMODES.paramSet},${CHANMODES.paramAlways},${CHANMODES.boolean}`,
|
|
@@ -73,6 +104,28 @@ function buildTokens(cfg: ServerConfig): string[] {
|
|
|
73
104
|
// per-connection opt-in). LIST is always non-destructive here so the
|
|
74
105
|
// token is unconditional.
|
|
75
106
|
'SAFELIST',
|
|
107
|
+
// IRCv3 MONITOR cap ceiling. The value matches the per-connection
|
|
108
|
+
// watchlist size enforced by the monitor reducer; deployments lower it
|
|
109
|
+
// via `serverConfig.monitorLimit`.
|
|
110
|
+
`MONITOR=${monitorLimit}`,
|
|
111
|
+
// STATUSMSG — prefixes a sender may target (`@#chan`/`+#chan`) for
|
|
112
|
+
// rank-scoped delivery. The sigil set mirrors PREFIX.
|
|
113
|
+
`STATUSMSG=${STATUSMSG_PREFIXES}`,
|
|
114
|
+
// EXTBAN — extended-ban prefix and the base type list (`q` = quiet).
|
|
115
|
+
// The prefix is config-derived. The account-name extban is advertised
|
|
116
|
+
// as a separate `ACCOUNTEXTBAN=a` token (below) when services ship.
|
|
117
|
+
`EXTBAN=${extbanPrefix},${EXTBAN_TYPES}`,
|
|
118
|
+
// draft/typing advertisement — the cap is always supported, so the
|
|
119
|
+
// token is unconditional. (Draft caps have no RFC ISUPPORT token; this
|
|
120
|
+
// is the local marker clients can key on.)
|
|
121
|
+
'TYPING',
|
|
122
|
+
// draft/multiline byte budget — mirrors the `draft/multiline=<n>` cap
|
|
123
|
+
// value so the ceiling is visible pre-cap-negotiation.
|
|
124
|
+
`MULTILINE=${multilineMax}`,
|
|
125
|
+
// ACCOUNTEXTBAN — account-name extended ban. Meaningful only once
|
|
126
|
+
// services register nicks, so suppressed until a deployment wires
|
|
127
|
+
// services up via `serverConfig.servicesEnabled`.
|
|
128
|
+
...(cfg.servicesEnabled ? [`ACCOUNTEXTBAN=${ACCOUNTEXTBAN_TYPE}`] : []),
|
|
76
129
|
];
|
|
77
130
|
}
|
|
78
131
|
|
|
@@ -82,6 +135,10 @@ function buildTokens(cfg: ServerConfig): string[] {
|
|
|
82
135
|
* batch. The caller appends the trailer separately.
|
|
83
136
|
*
|
|
84
137
|
* Each batch returns just the joined token string (no `005`, no trailer).
|
|
138
|
+
*
|
|
139
|
+
* Contract: `tokens` is non-empty. The only caller (`generateIsupport`)
|
|
140
|
+
* satisfies this — `buildTokens` always emits at least the network/casing
|
|
141
|
+
* tokens — so no defensive empty-input handling is needed here.
|
|
85
142
|
*/
|
|
86
143
|
function packTokens(tokens: ReadonlyArray<string>, baseLen: number): string[] {
|
|
87
144
|
const batches: string[] = [];
|
|
@@ -96,7 +153,7 @@ function packTokens(tokens: ReadonlyArray<string>, baseLen: number): string[] {
|
|
|
96
153
|
current = candidate;
|
|
97
154
|
}
|
|
98
155
|
}
|
|
99
|
-
|
|
156
|
+
batches.push(current);
|
|
100
157
|
return batches;
|
|
101
158
|
}
|
|
102
159
|
|
|
@@ -106,7 +163,7 @@ function packTokens(tokens: ReadonlyArray<string>, baseLen: number): string[] {
|
|
|
106
163
|
* @param cfg Server configuration (size limits, network name).
|
|
107
164
|
* @param _connCaps IRCv3 capabilities negotiated by the connection
|
|
108
165
|
* (reserved for `BOT=` and similar v3 tokens; currently
|
|
109
|
-
* unused — kept in the signature
|
|
166
|
+
* unused — kept in the signature for forward compatibility).
|
|
110
167
|
* @param nick Optional nick for the reply target; defaults to `*`.
|
|
111
168
|
* @returns One or more {@link RawLine}s, each within the 510-byte ceiling.
|
|
112
169
|
* Only the final line carries the `:are supported by this server`
|
|
@@ -35,12 +35,10 @@ function isValidChannelName(name: string, maxLen: number): boolean {
|
|
|
35
35
|
* nick (or `*` when unregistered). Optional `middle` is emitted before the
|
|
36
36
|
* trailing `:`-prefixed text.
|
|
37
37
|
*/
|
|
38
|
-
function numericErr(ctx: Ctx, code: number, trailing: string, middle
|
|
38
|
+
function numericErr(ctx: Ctx, code: number, trailing: string, middle: string): RawLine {
|
|
39
39
|
const nick = ctx.connection.nick ?? '*';
|
|
40
40
|
const codeStr = code.toString().padStart(3, '0');
|
|
41
|
-
const parts = [`:${ctx.serverName}`, codeStr, nick];
|
|
42
|
-
if (middle !== undefined) parts.push(middle);
|
|
43
|
-
parts.push(`:${trailing}`);
|
|
41
|
+
const parts = [`:${ctx.serverName}`, codeStr, nick, middle, `:${trailing}`];
|
|
44
42
|
return { text: parts.join(' ') };
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for the IRC `KILL` command (oper-gated force-disconnect).
|
|
3
|
+
*
|
|
4
|
+
* PLAN §2.1 location-of-authority: KILL runs against the *target*'s
|
|
5
|
+
* connection. The actor layer resolves `<nick>` to a live
|
|
6
|
+
* {@link ConnectionState} via the nick registry and passes it here. The
|
|
7
|
+
* reducer performs the oper gate, validates parameters, and emits a single
|
|
8
|
+
* {@link DisconnectEffect} for the target. The runtime's `disconnect`
|
|
9
|
+
* method is responsible for closing the target's transport and fanning the
|
|
10
|
+
* QUIT out to shared-channel peers — the reducer does not model that
|
|
11
|
+
* fanout itself.
|
|
12
|
+
*
|
|
13
|
+
* Wire format (RFC 2812 §4.6.1):
|
|
14
|
+
* - `KILL <nickname> <comment>`
|
|
15
|
+
* - The target receives an `ERROR` / `QUIT` carrying a reason formatted
|
|
16
|
+
* as `Killed (<oper> (<comment>))`.
|
|
17
|
+
* - `481 ERR_NOPRIVILEGES`: `:<server> 481 <nick> :Permission Denied - ...`
|
|
18
|
+
* - `461 ERR_NEEDMOREPARAMS`:
|
|
19
|
+
* `:<server> 461 <nick> KILL :Not enough parameters`
|
|
20
|
+
* - `401 ERR_NOSUCHNICK`: `:<server> 401 <nick> <target> :No such nick/channel`
|
|
21
|
+
*
|
|
22
|
+
* Behaviour:
|
|
23
|
+
* - Non-oper sender → `481` (checked before parameters so a non-oper
|
|
24
|
+
* cannot probe which nicks are online).
|
|
25
|
+
* - Missing `<nickname>` or `<comment>` → `461`.
|
|
26
|
+
* - Unknown nick → `401`.
|
|
27
|
+
* - Otherwise emit `Disconnect(targetConnId, "Killed (<oper> (<comment>))")`.
|
|
28
|
+
* - An oper may `KILL` themselves (RFC-permitted); no special-casing.
|
|
29
|
+
*
|
|
30
|
+
* Platform asymmetry: the emitted `Disconnect` is interpreted by the bound
|
|
31
|
+
* runtime. On CF the `disconnect` RPC closes the target DO's socket
|
|
32
|
+
* immediately; on AWS a `$default` invocation cannot close a remote APIGW
|
|
33
|
+
* WebSocket, so QUIT fanout to peers fires but the target's live socket
|
|
34
|
+
* only closes on its next `$disconnect`/sweeper cycle. The reducer is
|
|
35
|
+
* identical across adapters — only the runtime's `disconnect` semantics
|
|
36
|
+
* differ.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { Effect } from '../effects.js';
|
|
40
|
+
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
41
|
+
import { Numerics } from '../protocol/numerics.js';
|
|
42
|
+
import type { ConnectionState } from '../state/connection.js';
|
|
43
|
+
import type { Ctx } from '../types.js';
|
|
44
|
+
|
|
45
|
+
/** Fixed text emitted for `481 ERR_NOPRIVILEGES`. */
|
|
46
|
+
const NOPRIVILEGES_TRAILING_TEXT = "Permission Denied - You're not an IRC operator";
|
|
47
|
+
|
|
48
|
+
/** Fixed text emitted for `461 ERR_NEEDMOREPARAMS`. */
|
|
49
|
+
const NEEDMOREPARAMS_TRAILING_TEXT = 'Not enough parameters';
|
|
50
|
+
|
|
51
|
+
/** Fixed text emitted for `401 ERR_NOSUCHNICK`. */
|
|
52
|
+
const NOSUCHNICK_TRAILING_TEXT = 'No such nick/channel';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Formats the canonical kill reason, `Killed (<oper> (<comment>))`, that
|
|
56
|
+
* the target receives on its closing transport. `<oper>` is the killing
|
|
57
|
+
* oper's display nick; `<comment>` is the oper-supplied reason verbatim.
|
|
58
|
+
*/
|
|
59
|
+
export function formatKillReason(operNick: string, comment: string): string {
|
|
60
|
+
return `Killed (${operNick} (${comment}))`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Handles `KILL <nick> <comment>`.
|
|
65
|
+
*
|
|
66
|
+
* `target` is the live {@link ConnectionState} the actor resolved for
|
|
67
|
+
* `<nick>`, or `undefined` when the nick is offline / unknown. The
|
|
68
|
+
* signature is bespoke (not {@link Reducer}) because the actor must
|
|
69
|
+
* resolve the target asynchronously — mirrors `whoisReducer`.
|
|
70
|
+
*/
|
|
71
|
+
export function killReducer(
|
|
72
|
+
target: ConnectionState | undefined,
|
|
73
|
+
msg: { command: string; params: readonly string[] },
|
|
74
|
+
ctx: Ctx,
|
|
75
|
+
): { effects: EffectType[] } {
|
|
76
|
+
ctx.connection.lastSeen = ctx.clock.now();
|
|
77
|
+
|
|
78
|
+
if (!ctx.connection.userModes.oper) {
|
|
79
|
+
return {
|
|
80
|
+
effects: [
|
|
81
|
+
Effect.send(ctx.connId, [
|
|
82
|
+
numericLine(ctx, Numerics.ERR_NOPRIVILEGES, NOPRIVILEGES_TRAILING_TEXT),
|
|
83
|
+
]),
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const targetNick = msg.params[0];
|
|
89
|
+
const comment = msg.params[1];
|
|
90
|
+
|
|
91
|
+
if (targetNick === undefined || comment === undefined) {
|
|
92
|
+
return {
|
|
93
|
+
effects: [
|
|
94
|
+
Effect.send(ctx.connId, [
|
|
95
|
+
numericLine(ctx, Numerics.ERR_NEEDMOREPARAMS, NEEDMOREPARAMS_TRAILING_TEXT, 'KILL'),
|
|
96
|
+
]),
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (target === undefined) {
|
|
102
|
+
return {
|
|
103
|
+
effects: [
|
|
104
|
+
Effect.send(ctx.connId, [
|
|
105
|
+
numericLine(ctx, Numerics.ERR_NOSUCHNICK, NOSUCHNICK_TRAILING_TEXT, targetNick),
|
|
106
|
+
]),
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const operNick = ctx.connection.nick ?? '*';
|
|
112
|
+
return { effects: [Effect.disconnect(target.id, formatKillReason(operNick, comment))] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Builds a `:<server> <code> <nick> [<middle>] :<trailing>` line. The nick
|
|
117
|
+
* falls back to `*` when the connection has not yet registered one,
|
|
118
|
+
* matching the rest of the codebase's numeric formatting.
|
|
119
|
+
*/
|
|
120
|
+
function numericLine(ctx: Ctx, code: number, trailing: string, middle?: string): RawLine {
|
|
121
|
+
const nick = ctx.connection.nick ?? '*';
|
|
122
|
+
const codeStr = code.toString().padStart(3, '0');
|
|
123
|
+
const parts = [`:${ctx.serverName}`, codeStr, nick];
|
|
124
|
+
if (middle !== undefined) parts.push(middle);
|
|
125
|
+
parts.push(`:${trailing}`);
|
|
126
|
+
return { text: parts.join(' ') };
|
|
127
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - `322 RPL_LIST` — `:<server> 322 <nick> <chan> <#users> :<topic>`
|
|
12
12
|
* - `323 RPL_LISTEND` — `:<server> 323 <nick> :End of /LIST`
|
|
13
13
|
*
|
|
14
|
-
* Visibility rules
|
|
14
|
+
* Visibility rules:
|
|
15
15
|
* - Secret (`+s`) channels are hidden from non-members.
|
|
16
16
|
* - Membership is checked against `chan.members` using `ctx.connId`.
|
|
17
17
|
*
|