serverless-ircd 0.4.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/CHANGELOG.md +175 -0
- package/README.md +88 -19
- package/apps/aws-stack/README.md +4 -3
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +32 -4
- package/apps/aws-stack/tests/stack.test.ts +47 -1
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-worker/package.json +1 -1
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +129 -21
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -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 +63 -5
- package/packages/aws-adapter/src/handlers/index.ts +41 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/index.ts +2 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- 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/connect.test.ts +97 -1
- package/packages/aws-adapter/tests/handlers.test.ts +148 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +2 -0
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/cf-adapter/package.json +5 -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 +182 -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/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 +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- 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-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/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 +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/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 +1 -1
- package/packages/irc-core/src/commands/index.ts +8 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- 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/rehash.ts +119 -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/wallops.ts +118 -0
- package/packages/irc-core/src/config.ts +7 -0
- package/packages/irc-core/src/effects.ts +27 -1
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +179 -0
- package/packages/irc-core/src/protocol/numerics.ts +42 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +8 -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/tests/admission.test.ts +18 -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/rehash.test.ts +171 -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/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- 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/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/stats-store.test.ts +222 -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 +249 -16
- package/packages/irc-server/src/dispatch.ts +1 -0
- 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 +1089 -1
- package/packages/irc-server/tests/dispatch.test.ts +37 -0
- 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/scenarios.ts +9 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +1 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,181 @@ For the release process itself — versioning policy, pre-release checklist,
|
|
|
10
10
|
cutting a tag, rolling back — see [`docs/release.md`](docs/release.md).
|
|
11
11
|
Cross-reference `progress.md` / `tickets.md` for per-ticket detail.
|
|
12
12
|
|
|
13
|
+
## [0.5.0] - 2026-08-02
|
|
14
|
+
|
|
15
|
+
The headline is **protocol completion**: the deferred IRC verbs land
|
|
16
|
+
(Phase 11), and the S2S / obsolete RFC 2812 verbs are **formally
|
|
17
|
+
dropped** rather than left "deferred". With this release every standard
|
|
18
|
+
IRC verb a modern client expects is implemented — the only remaining
|
|
19
|
+
`421 ERR_UNKNOWNCOMMAND` paths are the deliberately-unimplemented S2S
|
|
20
|
+
and obsolete verbs. The roadmap's 0.x protocol surface is complete;
|
|
21
|
+
what remains is the load/compat sweep (10k-connection load test +
|
|
22
|
+
client compatibility matrix) and the required-`serverName` config gate.
|
|
23
|
+
|
|
24
|
+
### Added — Deferred IRC verbs (`@serverless-ircd/irc-core`, `@serverless-ircd/irc-server`)
|
|
25
|
+
|
|
26
|
+
Six Phase 11 tickets light up the remaining command surface. Each ships
|
|
27
|
+
a pure reducer, an actor route case (no more `421`), new numerics in
|
|
28
|
+
`protocol/numerics.ts`, and unit + actor coverage. (The remaining two
|
|
29
|
+
Phase 11 tickets are drops — see below.)
|
|
30
|
+
|
|
31
|
+
- **`KILL`** (oper-gated force-disconnect): `KILL <nick>
|
|
32
|
+
<comment>` resolves the target via the nick registry + a live
|
|
33
|
+
connection fetch (mirroring WHOIS), checks the oper gate (`481
|
|
34
|
+
ERR_NOPRIVILEGES`), and emits a `Disconnect` effect carrying the
|
|
35
|
+
classic `Killed (<oper> (<comment>))` reason. Peer `QUIT` fanout in
|
|
36
|
+
the target's shared channels happens via the runtime's existing
|
|
37
|
+
`disconnect` path. Missing params → `461`; unknown nick → `401`; an
|
|
38
|
+
oper may `KILL` themselves (RFC-permitted).
|
|
39
|
+
**Platform asymmetry:** on Cloudflare the `disconnect` RPC closes the
|
|
40
|
+
target DO's socket immediately; on AWS a `$default` Lambda invocation
|
|
41
|
+
cannot close a *remote* APIGW WebSocket, so the target's
|
|
42
|
+
live socket only closes on its next `$disconnect` / sweeper cycle —
|
|
43
|
+
the QUIT fanout to peers still fires. The reducer is identical; the
|
|
44
|
+
difference is the bound runtime's `disconnect` semantics.
|
|
45
|
+
- **`REHASH`** (oper-gated config reload): emits `382
|
|
46
|
+
RPL_REHASHING :<source>` and re-fetches `ServerConfig` from the
|
|
47
|
+
adapter's bound source via a new `IrcRuntime.reloadConfig()` port
|
|
48
|
+
method. The refreshed value swaps into the actor's live config
|
|
49
|
+
reference so subsequent commands observe it (rotated oper passwords,
|
|
50
|
+
new MOTD source, etc.). On reload failure the previous config stays
|
|
51
|
+
in effect and a graceful error-suffixed `382` follows — the oper is
|
|
52
|
+
**not** disconnected (a bad store read must never sever a live link).
|
|
53
|
+
Already-open connections keep their negotiated caps and registration;
|
|
54
|
+
transport/TLS reload requires a redeploy.
|
|
55
|
+
- **`LUSERS` + `STATS`** (network statistics): backed by a
|
|
56
|
+
new async `ServerStats` port (`getStats(): Promise<ServerStatsSnapshot>`)
|
|
57
|
+
and an `InMemoryStats` reference impl. `LUSERS` emits the full
|
|
58
|
+
`251`–`255` sequence (only the non-zero counts, per RFC) plus the
|
|
59
|
+
optional `265` / `266` local/global counts. `STATS <query>` supports
|
|
60
|
+
the `u` (uptime, `242 RPL_STATSUPTIME`) and `l` (link info,
|
|
61
|
+
`211 RPL_STATSLINKINFO`) letters; unknown letters get the
|
|
62
|
+
charybdis-style empty body terminated by `219 RPL_ENDOFSTATS`. A
|
|
63
|
+
`<server>` argument that does not match the local server (case-
|
|
64
|
+
insensitive) yields `402 ERR_NOSUCHSERVER` (single-server network —
|
|
65
|
+
S2S aggregation is a PLAN non-goal). The `computeStatsSnapshot`
|
|
66
|
+
helper centralizes the oper/invisible/unknown classification so each
|
|
67
|
+
adapter's backend only has to gather raw counts.
|
|
68
|
+
- **`TRACE`** (routing diagnostics): in a single-server
|
|
69
|
+
serverless IRCd `TRACE` collapses to a local-server line + the
|
|
70
|
+
`262 RPL_ENDOFTRACE` terminator. Per-connection `204
|
|
71
|
+
RPL_TRACEOPERATOR` / `205 RPL_TRACEUSER` detail is **oper-gated** —
|
|
72
|
+
non-opers see only the server line + `262` (no enumeration of `+i`
|
|
73
|
+
users they don't share a channel with). `TRACE <server>` where
|
|
74
|
+
`<server>` is not the local server → `402`.
|
|
75
|
+
- **`WALLOPS`** (oper broadcast): `WALLOPS :<message>` is
|
|
76
|
+
oper-only to send (modern ircd convention — non-oper → `481`);
|
|
77
|
+
delivery fans out to every connection whose `+w` user mode is set via
|
|
78
|
+
a new `BroadcastWallops` effect interpreted by the runtime's
|
|
79
|
+
`broadcastWallops` method (global cross-connection fanout, not
|
|
80
|
+
channel-scoped). The originator is always excluded (skip-self,
|
|
81
|
+
matching NOTICE/PRIVMSG semantics). Empty message → `461`. The `+w`
|
|
82
|
+
user mode was already accepted by `MODE` and advertised in `004
|
|
83
|
+
RPL_MYINFO`; this lights up the consumer.
|
|
84
|
+
- **`SETNAME`** (realname change): `SETNAME :<realname>`
|
|
85
|
+
updates `state.realname` so a subsequent `WHOIS` reflects the new
|
|
86
|
+
value in `311 RPL_WHOISUSER`. No broadcast is emitted in v1 (there
|
|
87
|
+
is no standard IRCv3 cap for relaying a realname change — a
|
|
88
|
+
`draft/setname` relay is a documented follow-up). Empty/missing
|
|
89
|
+
realname → `461`; the registration-time realname length cap applies.
|
|
90
|
+
|
|
91
|
+
### Changed — S2S & obsolete verbs formally dropped (`@serverless-ircd/irc-core`)
|
|
92
|
+
|
|
93
|
+
The remaining two Phase 11 tickets convert the "deferred" framing into
|
|
94
|
+
an explicit **drop**. There is no behaviour change on the wire — the verbs
|
|
95
|
+
already returned `421 ERR_UNKNOWNCOMMAND` — but the framing, comments,
|
|
96
|
+
and reserved numerics are cleaned up so the codebase no longer carries
|
|
97
|
+
dead scaffolding.
|
|
98
|
+
|
|
99
|
+
- **S2S verbs** (`CONNECT`, `SQUIT`, `LINKS`): dropped as a PLAN
|
|
100
|
+
non-goal (single-server serverless IRCd; no mesh to connect, split,
|
|
101
|
+
or list). The now-unused numerics `RPL_LINKS (364)`,
|
|
102
|
+
`RPL_ENDOFLINKS (365)`, and `ERR_CANTKILLSERVER (483)` are removed
|
|
103
|
+
from `numerics.ts` and its auto-derived `numericToName` reverse map.
|
|
104
|
+
`ERR_NOSUCHSERVER (402)` is retained — `LUSERS` / `STATS` / `TRACE`
|
|
105
|
+
use it for remote-server gating.
|
|
106
|
+
- **Obsolete RFC 2812 verbs** (`SERVICE`, `SUMMON`, `USERS`): dropped
|
|
107
|
+
(never widely implemented; no modern client uses them). The now-unused
|
|
108
|
+
numerics `RPL_YOURESERVICE (383)`, `ERR_NOSUCHSERVICE (408)`,
|
|
109
|
+
`ERR_SUMMONDISABLED (445)`, and `ERR_USERSDISABLED (446)` are removed.
|
|
110
|
+
- The `actor.ts` default-branch triage comment was rewritten: the
|
|
111
|
+
six verbs are documented as **deliberately unimplemented** rather
|
|
112
|
+
than "deferred".
|
|
113
|
+
- Source-level regression guards
|
|
114
|
+
(`packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts`)
|
|
115
|
+
and a behaviour guard in `actor.test.ts` (the verbs still emit
|
|
116
|
+
`421`) pin the drop so the numerics or the "deferred" framing cannot
|
|
117
|
+
creep back in.
|
|
118
|
+
|
|
119
|
+
### Added — Runtime ports for the new verbs
|
|
120
|
+
|
|
121
|
+
- **`ServerStats` port** (`packages/irc-core/src/ports.ts`): async
|
|
122
|
+
`getStats()` returning `ServerStatsSnapshot` (users / invisible /
|
|
123
|
+
opers / unknown / channels / servers / local+global conn counts /
|
|
124
|
+
uptime anchor). Asynchronous because the aggregation crosses every
|
|
125
|
+
connection and channel — it cannot be pre-loaded into a synchronous
|
|
126
|
+
view the way MOTD or a credential table can. Each adapter implements
|
|
127
|
+
it against its authoritative store; `InMemoryStats` is the reference
|
|
128
|
+
impl. The `computeStatsSnapshot` helper holds the shared
|
|
129
|
+
classification logic (oper / invisible / unknown).
|
|
130
|
+
- **`reloadConfig()` on `IrcRuntime`** (`packages/irc-server/src/runtime.ts`):
|
|
131
|
+
request/response port method the `REHASH` route calls to re-fetch
|
|
132
|
+
`ServerConfig` from the adapter's bound source (CF: KV/secret env;
|
|
133
|
+
AWS: Secrets Manager/SSM env; local-cli: the config file/options).
|
|
134
|
+
Modelled as a runtime method rather than a fire-and-forget effect
|
|
135
|
+
because the refreshed value must come back to the actor.
|
|
136
|
+
|
|
137
|
+
### ⚠️ Migration required
|
|
138
|
+
|
|
139
|
+
- **For adapter authors wiring the new ports.** `ServerStats` and
|
|
140
|
+
`reloadConfig` are optional at the type level (the actor falls back
|
|
141
|
+
to a zero-count snapshot when no `ServerStats` is bound, and
|
|
142
|
+
`REHASH` still emits `382` even if `reloadConfig` rejects — the
|
|
143
|
+
previous config is retained). To surface real counts and live
|
|
144
|
+
reloads, each adapter wires its own backend. No Durable-Object
|
|
145
|
+
persisted-state schema change (`PERSISTED_STATE_VERSION` stays `1`);
|
|
146
|
+
code rollback across this release is safe.
|
|
147
|
+
- No toolchain change. Node ≥ 24 / pnpm 11 (from v0.4.0) still apply.
|
|
148
|
+
|
|
149
|
+
### Known limitations
|
|
150
|
+
|
|
151
|
+
- Same as v0.4.0's transport limits: the **CF TCP path requires
|
|
152
|
+
Cloudflare Spectrum (Enterprise tier)** plus a stateful Container
|
|
153
|
+
origin; the **AWS TCP path** uses NLB + Lambda streaming and is
|
|
154
|
+
subject to Lambda idle-timeout / stream-duration limits. The
|
|
155
|
+
WebSocket path remains the zero-extra-deps default.
|
|
156
|
+
- **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
|
|
157
|
+
CA/trust-store** on both platforms and is not enabled by default.
|
|
158
|
+
- `serverName` still defaults to the `irc.example.com` placeholder when
|
|
159
|
+
an adapter config omits it (pending).
|
|
160
|
+
- `STATS` ships the `u` + `l` query letters; the full RFC matrix
|
|
161
|
+
(`k`, `K`, `q`, `g`, `x`, `z`, …) is intentionally out of scope for
|
|
162
|
+
v1 — unknown letters get the charybdis-style empty body + `219`.
|
|
163
|
+
- Load testing (10k concurrent connections) and the client
|
|
164
|
+
compatibility sweep (WeeChat / HexChat / IRCCloud / TheLounge)
|
|
165
|
+
remain pending.
|
|
166
|
+
- **Local coverage instrumentation on the Cloudflare-backed packages
|
|
167
|
+
(`@serverless-ircd/cf-adapter`, `@serverless-ircd/cf-worker`) is
|
|
168
|
+
incompatible with the current vitest toolchain.** `@vitest/coverage-v8`
|
|
169
|
+
v4.1.x imports `node:inspector/promises`, which the
|
|
170
|
+
`@cloudflare/vitest-pool-workers` (workerd) test pool cannot resolve.
|
|
171
|
+
The packages' test suites themselves pass (cf-adapter 185/185,
|
|
172
|
+
cf-worker 3/3); only `pnpm coverage` fails to load the coverage
|
|
173
|
+
module under the workerd pool. This is a documented [vitest-pool-workers
|
|
174
|
+
known issue](https://developers.cloudflare.com/workers/testing/vitest-integration/known-issues/#module-resolution)
|
|
175
|
+
and is expected to resolve with an upstream `@vitest/coverage-v8` or
|
|
176
|
+
pool-workers release. No production code is affected; CI behavior
|
|
177
|
+
depends on its installed versions.
|
|
178
|
+
|
|
179
|
+
### Security
|
|
180
|
+
|
|
181
|
+
- None. No vulnerabilities have been reported or fixed in this release.
|
|
182
|
+
The dropped S2S / obsolete verbs and their reserved numerics carried
|
|
183
|
+
no reachable code path; their removal reduces surface area but fixes
|
|
184
|
+
no defect.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
13
188
|
## [0.4.0] - 2026-07-31
|
|
14
189
|
|
|
15
190
|
The headline is **dual transport**: both adapters now speak a real
|
package/README.md
CHANGED
|
@@ -6,21 +6,24 @@ platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
|
|
|
6
6
|
|
|
7
7
|
One TypeScript codebase. Two serverless substrates.
|
|
8
8
|
|
|
9
|
-
> **Status:** **v0.
|
|
9
|
+
> **Status:** **v0.5.0 (preview).** The pure protocol core, the
|
|
10
10
|
> `IrcRuntime` port + in-memory runtime, a runnable local CLI server,
|
|
11
11
|
> the **Cloudflare Workers** adapter, and the **AWS** (API Gateway
|
|
12
12
|
> WebSocket + Lambda + DynamoDB + CDK) adapter are all functional and
|
|
13
|
-
> deployed to staging. v0.4.0
|
|
14
|
-
>
|
|
13
|
+
> deployed to staging. v0.4.0 shipped **dual transport**: both adapters
|
|
14
|
+
> speak a real `irc+tls :6697` (TLS-over-TCP) surface for stock IRC
|
|
15
15
|
> clients — Cloudflare via Spectrum + a Container origin, AWS via a
|
|
16
16
|
> Network Load Balancer + Lambda streaming — alongside the WebSocket
|
|
17
|
-
> path
|
|
18
|
-
>
|
|
19
|
-
> `
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
-
>
|
|
23
|
-
>
|
|
17
|
+
> path, plus **SASL `EXTERNAL` via mTLS** and a **Cloudflare D1-backed
|
|
18
|
+
> SASL account store**. v0.5.0 completes the **protocol surface**: the
|
|
19
|
+
> deferred IRC verbs (`KILL`, `REHASH`, `LUSERS` + `STATS`, `TRACE`,
|
|
20
|
+
> `WALLOPS`, `SETNAME`) all land, and the S2S (`CONNECT` / `SQUIT` /
|
|
21
|
+
> `LINKS`) and obsolete RFC 2812 (`SERVICE` / `SUMMON` / `USERS`) verbs
|
|
22
|
+
> are formally dropped — every standard verb a modern client expects is
|
|
23
|
+
> now implemented. Remaining 0.x work: the load/compat sweep
|
|
24
|
+
> (10k-connection load test + client compatibility matrix) and a
|
|
25
|
+
> required-`serverName` config gate. See `CHANGELOG.md` for the
|
|
26
|
+
> per-release manifests.
|
|
24
27
|
|
|
25
28
|
---
|
|
26
29
|
|
|
@@ -299,6 +302,42 @@ against any line-oriented WebSocket endpoint.
|
|
|
299
302
|
|
|
300
303
|
---
|
|
301
304
|
|
|
305
|
+
## WebSocket transport (IRCv3 subprotocols)
|
|
306
|
+
|
|
307
|
+
The WebSocket entry points (the local CLI, the Cloudflare Worker, and the
|
|
308
|
+
AWS API Gateway `$connect` route) implement the
|
|
309
|
+
[IRCv3 WebSocket support](https://ircv3.net/specs/websocket.html) subprotocol
|
|
310
|
+
negotiation. A client advertises support by offering one of two registered
|
|
311
|
+
subprotocols in its opening handshake's `Sec-WebSocket-Protocol` header:
|
|
312
|
+
|
|
313
|
+
| Subprotocol | Frames | Notes |
|
|
314
|
+
|--------------------|----------|--------------------------------------------------|
|
|
315
|
+
| `text.ircv3.net` | UTF-8 text | Lone surrogates → U+FFFD; binary frames rejected (close `1003`). |
|
|
316
|
+
| `binary.ircv3.net` | binary | UTF-8 encoded bytes; same framing as `text`. |
|
|
317
|
+
|
|
318
|
+
The server echoes back the first supported entry in client-preference order
|
|
319
|
+
(in the `101` response's `Sec-WebSocket-Protocol` header). Once negotiated,
|
|
320
|
+
the connection uses **spec framing**:
|
|
321
|
+
|
|
322
|
+
- **One IRC message per WebSocket message** — a frame is never split on an
|
|
323
|
+
embedded line break. A single optional trailing `CR-LF` (or bare `LF`) is
|
|
324
|
+
stripped; the remainder is exactly one IRC line.
|
|
325
|
+
- **No trailing `CR-LF` on the wire**, outbound or inbound.
|
|
326
|
+
- **510-byte message budget** — the 512-byte IRC line limit minus the omitted
|
|
327
|
+
`CR-LF`. A message exceeding 510 bytes is a protocol violation and the
|
|
328
|
+
connection is closed with RFC 6455 code `1009` (Message Too Big).
|
|
329
|
+
|
|
330
|
+
**Legacy fallback.** A client that offers no recognized subprotocol (or none
|
|
331
|
+
at all) is still accepted, in **legacy mode**: frames are split on `\r\n` so
|
|
332
|
+
older clients that concatenate several messages into one frame keep working,
|
|
333
|
+
and the parser's existing 512-byte line cap applies. Nothing breaks until a
|
|
334
|
+
client opts into a subprotocol. The in-tree
|
|
335
|
+
[`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder)
|
|
336
|
+
offers `binary.ircv3.net` upstream, so a stock TCP IRC client bridged to a
|
|
337
|
+
deployed stack exercises the spec path end-to-end.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
302
341
|
## Testing strategy
|
|
303
342
|
|
|
304
343
|
This project follows strict TDD (Red → Green → Refactor) — every reducer is
|
|
@@ -345,9 +384,28 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
|
345
384
|
✅ landed in **v0.4.0**: generalized `ConnectionActor` transport seam,
|
|
346
385
|
Cloudflare Spectrum + Container origin, AWS NLB + Lambda streaming,
|
|
347
386
|
mTLS → SASL `EXTERNAL`, transport-parametrized contract suite, ADR-009.
|
|
348
|
-
- **Phase 7** — Load testing (10k concurrent connections per platform)
|
|
349
|
-
client compatibility sweep (WeeChat, HexChat, IRCCloud, TheLounge)
|
|
350
|
-
|
|
387
|
+
- **Phase 7** — Load testing (10k concurrent connections per platform)
|
|
388
|
+
and client compatibility sweep (WeeChat, HexChat, IRCCloud, TheLounge).
|
|
389
|
+
⬳ pending; the dual-transport surface it needs
|
|
390
|
+
landed in v0.4.0.
|
|
391
|
+
- **Phase 8 — PLAN-FIXES remediation.** ✅ landed across v0.3.0–v0.4.0
|
|
392
|
+
(MessageStore wired into all three adapters, AccountStore end-to-end,
|
|
393
|
+
the DynamoDB `Accounts` table decided, CF enumeration RPCs, AWS QUIT
|
|
394
|
+
fanout, `OPER` / `TAGMSG` / `WHOWAS`, RFC 1459 case-mapping, real
|
|
395
|
+
MOTD, config-driven server identity, AWS max-clients admission,
|
|
396
|
+
CF D1 SASL store, and a sweep of dead-code / stale-comment cleanup).
|
|
397
|
+
- **Phase 9 — Runtime & tooling baseline.** ✅ landed in **v0.4.0**
|
|
398
|
+
(Node ≥ 24 / pnpm 11).
|
|
399
|
+
- **Phase 10 — IRCv3 WebSocket extension.** ✅ landed in **v0.4.0**:
|
|
400
|
+
`text.ircv3.net` / `binary.ircv3.net` subprotocol negotiation +
|
|
401
|
+
per-message framing across the CF Worker, AWS APIGW, local CLI, and
|
|
402
|
+
`tcp-ws-forwarder` (510-byte budget, `1009`/`1003` close codes,
|
|
403
|
+
legacy fallback).
|
|
404
|
+
- **Phase 11 — Deferred IRC verbs.** ✅ landed in **v0.5.0**: `KILL`,
|
|
405
|
+
`REHASH`, `LUSERS` + `STATS`, `TRACE`, `WALLOPS`, `SETNAME`, plus
|
|
406
|
+
the formal drop of the S2S (`CONNECT`/`SQUIT`/`LINKS`) and obsolete
|
|
407
|
+
RFC 2812 (`SERVICE`/`SUMMON`/`USERS`) verbs. The protocol surface is
|
|
408
|
+
feature-complete for modern clients.
|
|
351
409
|
|
|
352
410
|
---
|
|
353
411
|
|
|
@@ -356,10 +414,19 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
|
356
414
|
**Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
|
|
357
415
|
`PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
|
|
358
416
|
channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`,
|
|
359
|
-
`WHOWAS`, `MOTD`, `AWAY`, `OPER` (credential auth → `o` user mode),
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
417
|
+
`WHOWAS`, `MOTD`, `AWAY`, `OPER` (credential auth → `o` user mode), the
|
|
418
|
+
query verbs `VERSION`, `TIME`, `ADMIN`, `INFO`, `USERHOST`, `ISON`, the
|
|
419
|
+
network-statistics verbs `LUSERS` and `STATS` (`u` uptime + `l` link-info
|
|
420
|
+
letters; unknown letters get the charybdis-style empty body), the
|
|
421
|
+
routing-diagnostic verb `TRACE` (oper-gated per-connection detail), the
|
|
422
|
+
realname-change verb `SETNAME`, and the oper verbs `KILL` (force-
|
|
423
|
+
disconnect), `REHASH` (config reload via `IrcRuntime.reloadConfig()`),
|
|
424
|
+
`WALLOPS` (global broadcast to every `+w` user).
|
|
425
|
+
|
|
426
|
+
(Out of scope: the S2S verbs `CONNECT`/`SQUIT`/`LINKS` and the obsolete
|
|
427
|
+
RFC 2812 verbs `SERVICE`/`SUMMON`/`USERS` are **formally dropped** as of
|
|
428
|
+
v0.5.0 — they return `421 ERR_UNKNOWNCOMMAND`, and their reserved
|
|
429
|
+
numerics have been removed. S2S linking is a PLAN non-goal.)
|
|
363
430
|
|
|
364
431
|
**Channel modes:** `o v b i k l t n m s p`.
|
|
365
432
|
**User modes:** `i`, `o` (local only), `w`, `s`.
|
|
@@ -388,9 +455,11 @@ bridges a stock TCP client to a deployed WebSocket endpoint.
|
|
|
388
455
|
- `CHANGELOG.md` — per-release manifests (Keep a Changelog format), including
|
|
389
456
|
the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
|
|
390
457
|
gates), v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
|
|
391
|
-
ChannelRegistryDO, SASL account persistence),
|
|
458
|
+
ChannelRegistryDO, SASL account persistence), v0.4.0 (dual transport
|
|
392
459
|
`irc+tls :6697`, SASL `EXTERNAL`/mTLS, WHOWAS + remaining verbs, D1 account
|
|
393
|
-
store, Node 24/pnpm 11).
|
|
460
|
+
store, Node 24/pnpm 11), and v0.5.0 (deferred IRC verbs `KILL` / `REHASH`
|
|
461
|
+
/ `LUSERS` + `STATS` / `TRACE` / `WALLOPS` / `SETNAME`, plus the formal
|
|
462
|
+
drop of the S2S and obsolete RFC 2812 verbs).
|
|
394
463
|
- `docs/Cloudflare-TCP-Deployment.md` and `docs/AWS-TCP-Deployment.md` —
|
|
395
464
|
end-to-end guides for the `:6697` TCP+TLS variants (Spectrum/Container on
|
|
396
465
|
CF, NLB + Lambda streaming on AWS), including mTLS trust-store setup.
|
package/apps/aws-stack/README.md
CHANGED
|
@@ -8,7 +8,7 @@ least-privilege IAM glue to let the Lambda read/write the tables and post back
|
|
|
8
8
|
to connected clients via the management API.
|
|
9
9
|
|
|
10
10
|
The Lambda handler is currently a stub (`{ statusCode: 200 }`); the real
|
|
11
|
-
`AwsRuntime` dispatch lands
|
|
11
|
+
`AwsRuntime` dispatch lands with the AWS runtime work.
|
|
12
12
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
@@ -32,7 +32,7 @@ Repo-level shortcuts (from the monorepo root):
|
|
|
32
32
|
```sh
|
|
33
33
|
pnpm deploy:aws:staging
|
|
34
34
|
pnpm deploy:aws:prod
|
|
35
|
-
pnpm smoke:aws:staging # deferred
|
|
35
|
+
pnpm smoke:aws:staging # deferred (smoke harness not yet wired)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Optional localstack validation
|
|
@@ -70,4 +70,5 @@ LOCALSTACK=1 pnpm test
|
|
|
70
70
|
## Handoff
|
|
71
71
|
|
|
72
72
|
Lambda runtime logic, DynamoDB access patterns, and the `AwsRuntime`
|
|
73
|
-
implementation
|
|
73
|
+
implementation land with the AWS runtime work. The scheduler/sweeper is
|
|
74
|
+
the EventBridge sweeper work.
|
|
@@ -17,7 +17,12 @@ import { resolve } from 'node:path';
|
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { TABLE_DEFS } from '@serverless-ircd/aws-adapter/cdk-table-defs';
|
|
19
19
|
import { CfnOutput, Duration, RemovalPolicy, Stack, type StackProps } from 'aws-cdk-lib';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
CfnIntegrationResponse,
|
|
22
|
+
WebSocketApi,
|
|
23
|
+
type WebSocketIntegration,
|
|
24
|
+
WebSocketStage,
|
|
25
|
+
} from 'aws-cdk-lib/aws-apigatewayv2';
|
|
21
26
|
import { WebSocketLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
|
22
27
|
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
23
28
|
import { Table } from 'aws-cdk-lib/aws-dynamodb';
|
|
@@ -191,9 +196,6 @@ export class IrcAwsStack extends Stack {
|
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
const webSocketApi = new WebSocketApi(this, 'IrcWsApi', {
|
|
194
|
-
connectRouteOptions: {
|
|
195
|
-
integration: new WebSocketLambdaIntegration('ConnectIntegration', handler),
|
|
196
|
-
},
|
|
197
199
|
disconnectRouteOptions: {
|
|
198
200
|
integration: new WebSocketLambdaIntegration('DisconnectIntegration', handler),
|
|
199
201
|
},
|
|
@@ -202,6 +204,32 @@ export class IrcAwsStack extends Stack {
|
|
|
202
204
|
},
|
|
203
205
|
});
|
|
204
206
|
|
|
207
|
+
// The `$connect` route is added explicitly (rather than via
|
|
208
|
+
// `connectRouteOptions`) so the negotiated IRCv3 WebSocket subprotocol can
|
|
209
|
+
// be echoed back to the client. API Gateway selects the WebSocket
|
|
210
|
+
// subprotocol from the `$connect` integration response, so the route is
|
|
211
|
+
// configured to return a response and an `IntegrationResponse` maps the
|
|
212
|
+
// `Sec-WebSocket-Protocol` header from the Lambda's reply onto the
|
|
213
|
+
// handshake. The Lambda reads the client's offer, selects the first
|
|
214
|
+
// supported entry, and returns it in its response header (see
|
|
215
|
+
// `handleConnect` / `dispatch`).
|
|
216
|
+
const connectRoute = webSocketApi.addRoute('$connect', {
|
|
217
|
+
integration: new WebSocketLambdaIntegration('ConnectIntegration', handler),
|
|
218
|
+
returnResponse: true,
|
|
219
|
+
});
|
|
220
|
+
const connectIntegration = connectRoute.node.findChild(
|
|
221
|
+
'ConnectIntegration',
|
|
222
|
+
) as WebSocketIntegration;
|
|
223
|
+
new CfnIntegrationResponse(this, 'ConnectSubprotocolResponse', {
|
|
224
|
+
apiId: webSocketApi.apiId,
|
|
225
|
+
integrationId: connectIntegration.integrationId,
|
|
226
|
+
integrationResponseKey: '$default',
|
|
227
|
+
responseParameters: {
|
|
228
|
+
'method.response.header.Sec-WebSocket-Protocol':
|
|
229
|
+
'integration.response.header.Sec-WebSocket-Protocol',
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
205
233
|
// The stage name is intentionally NOT suffixed with the environment:
|
|
206
234
|
// each environment is its own CloudFormation stack (and therefore its
|
|
207
235
|
// own WebSocket API), so `prod` in every env resolves to a distinct
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Synth-time assertions for the `apps/aws-stack` CDK stack.
|
|
3
3
|
*
|
|
4
|
-
* These are the primary red→green tests for the infrastructure
|
|
4
|
+
* These are the primary red→green tests for the infrastructure stack:
|
|
5
5
|
* they assert the exact CloudFormation shape that `cdk synth` must
|
|
6
6
|
* produce — five DynamoDB tables with the PLAN §4 schema, a single
|
|
7
7
|
* Node 24 Lambda wired to all three WebSocket routes, the API Gateway
|
|
@@ -149,6 +149,52 @@ describe('IrcAwsStack — API Gateway v2 WebSocket API', () => {
|
|
|
149
149
|
const { template } = makeTemplate();
|
|
150
150
|
template.resourceCountIs('AWS::ApiGatewayV2::Route', 3);
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
it('enables the $connect route to return a response (route response selection)', () => {
|
|
154
|
+
const { template } = makeTemplate();
|
|
155
|
+
template.hasResourceProperties('AWS::ApiGatewayV2::Route', {
|
|
156
|
+
RouteKey: '$connect',
|
|
157
|
+
RouteResponseSelectionExpression: '$default',
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('creates a $connect route response so the upgrade can carry a response', () => {
|
|
162
|
+
const { template } = makeTemplate();
|
|
163
|
+
template.hasResourceProperties('AWS::ApiGatewayV2::RouteResponse', {
|
|
164
|
+
RouteResponseKey: '$default',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('IrcAwsStack — IRCv3 WebSocket subprotocol echo', () => {
|
|
170
|
+
it('attaches an integration response on the $connect route mapping Sec-WebSocket-Protocol', () => {
|
|
171
|
+
const { template } = makeTemplate();
|
|
172
|
+
template.hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', {
|
|
173
|
+
ResponseParameters: {
|
|
174
|
+
'method.response.header.Sec-WebSocket-Protocol':
|
|
175
|
+
'integration.response.header.Sec-WebSocket-Protocol',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('binds the subprotocol integration response to the $connect integration', () => {
|
|
181
|
+
const { template, stack } = makeTemplate();
|
|
182
|
+
const responses = template.findResources('AWS::ApiGatewayV2::IntegrationResponse');
|
|
183
|
+
// Exactly one integration response is configured (the $connect subprotocol echo).
|
|
184
|
+
expect(Object.keys(responses)).toHaveLength(1);
|
|
185
|
+
// The IntegrationId Ref must resolve to a CfnIntegration under the $connect route.
|
|
186
|
+
const stackName = stack.stackName;
|
|
187
|
+
void stackName;
|
|
188
|
+
const cfnIntegrations = template.findResources('AWS::ApiGatewayV2::Integration');
|
|
189
|
+
expect(Object.keys(cfnIntegrations).length).toBeGreaterThanOrEqual(3);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('uses the $default integration response key', () => {
|
|
193
|
+
const { template } = makeTemplate();
|
|
194
|
+
template.hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', {
|
|
195
|
+
IntegrationResponseKey: '$default',
|
|
196
|
+
});
|
|
197
|
+
});
|
|
152
198
|
});
|
|
153
199
|
|
|
154
200
|
describe('IrcAwsStack — Lambda', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/cf-tcp-container",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Cloudflare Container TCP origin for the irc+tls :6697 transport — Spectrum terminates TLS at the edge, this container runs the IRC core over plaintext TCP",
|
|
6
6
|
"license": "BSD-3-Clause",
|