serverless-ircd 0.8.0 → 0.9.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 +4 -0
- package/CHANGELOG.md +245 -0
- package/README.md +160 -200
- package/apps/aws-stack/package.json +1 -1
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/container-server.ts +21 -1
- package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
- package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
- package/apps/cf-worker/package.json +1 -1
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +94 -31
- package/apps/local-cli/tests/config-resolution.test.ts +65 -0
- package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
- package/apps/local-cli/tests/rehash.test.ts +147 -0
- package/apps/local-cli/tests/server-helpers.test.ts +63 -0
- package/apps/local-cli/tests/tcp.test.ts +89 -0
- package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
- package/apps/web/landing/index.html +226 -3
- package/apps/web/package.json +2 -1
- package/apps/web/scripts/build.mjs +25 -2
- package/apps/web/src/render-docs.ts +292 -0
- package/apps/web/tests/build-smoke.test.ts +31 -2
- package/apps/web/tests/landing-content.test.ts +103 -0
- package/apps/web/tests/render-docs.test.ts +198 -0
- package/docs/AWS-Adapter-Architecture.md +3 -2
- package/docs/Services.md +33 -1
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +15 -1
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +10 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
- package/packages/aws-adapter/tests/connection-counter.test.ts +17 -0
- package/packages/aws-adapter/tests/global-setup.ts +28 -1
- package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
- package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
- package/packages/aws-adapter/tests/sweeper.test.ts +20 -0
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/connection-do.ts +18 -6
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +130 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/commands/account-auth.ts +46 -18
- package/packages/irc-core/src/commands/chanserv.ts +288 -4
- package/packages/irc-core/src/commands/hostserv.ts +38 -3
- package/packages/irc-core/src/commands/index.ts +1 -0
- package/packages/irc-core/src/commands/join.ts +41 -35
- package/packages/irc-core/src/commands/nickserv.ts +16 -4
- package/packages/irc-core/src/commands/registration.ts +27 -16
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/topic.ts +23 -10
- package/packages/irc-core/src/state/channel.ts +17 -0
- package/packages/irc-core/tests/commands/chanserv.test.ts +668 -1
- package/packages/irc-core/tests/commands/hostserv.test.ts +71 -0
- package/packages/irc-core/tests/commands/join.test.ts +179 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +185 -2
- package/packages/irc-core/tests/commands/registration.test.ts +227 -6
- package/packages/irc-core/tests/commands/sasl.test.ts +44 -0
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +80 -30
- package/packages/irc-server/tests/actor.test.ts +365 -3
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +8 -5
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +19 -0
- package/packages/irc-test-support/vitest.config.ts +6 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/load-test/src/client.ts +13 -13
- package/tools/load-test/tests/client.test.ts +258 -2
- package/tools/load-test/tests/config.test.ts +39 -0
- package/tools/load-test/tests/harness.test.ts +21 -0
- package/tools/load-test/tests/metrics.test.ts +7 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
- package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +51 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
package/README.md
CHANGED
|
@@ -6,48 +6,49 @@ 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:** **
|
|
10
|
-
>
|
|
11
|
-
>
|
|
9
|
+
> **Status:** **preview.** The pure protocol core, the `IrcRuntime`
|
|
10
|
+
> port + in-memory runtime, a runnable local CLI server, the
|
|
11
|
+
> **Cloudflare Workers** adapter, and the **AWS** (API Gateway
|
|
12
12
|
> WebSocket + Lambda + DynamoDB + CDK) adapter are all functional and
|
|
13
|
-
> deployed to staging.
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
>
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
-
>
|
|
23
|
-
>
|
|
24
|
-
> `
|
|
25
|
-
> `
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
>
|
|
33
|
-
>
|
|
34
|
-
>
|
|
35
|
-
>
|
|
36
|
-
>
|
|
37
|
-
>
|
|
38
|
-
>
|
|
39
|
-
>
|
|
40
|
-
>
|
|
41
|
-
>
|
|
42
|
-
>
|
|
43
|
-
>
|
|
44
|
-
>
|
|
45
|
-
>
|
|
46
|
-
>
|
|
47
|
-
>
|
|
48
|
-
>
|
|
49
|
-
>
|
|
50
|
-
>
|
|
13
|
+
> deployed to staging.
|
|
14
|
+
>
|
|
15
|
+
> **Dual transport.** Both adapters speak a real `irc+tls :6697`
|
|
16
|
+
> (TLS-over-TCP) surface for stock IRC clients — Cloudflare via
|
|
17
|
+
> Spectrum + a Container origin, AWS via a Network Load Balancer +
|
|
18
|
+
> Lambda streaming — alongside the WebSocket path, plus **SASL
|
|
19
|
+
> `EXTERNAL` via mTLS** and a **Cloudflare D1-backed SASL account
|
|
20
|
+
> store**.
|
|
21
|
+
>
|
|
22
|
+
> **Complete protocol surface.** The deferred IRC verbs (`KILL`,
|
|
23
|
+
> `REHASH`, `LUSERS` + `STATS`, `TRACE`, `WALLOPS`, `SETNAME`) are
|
|
24
|
+
> implemented, and the S2S (`CONNECT` / `SQUIT` / `LINKS`) and obsolete
|
|
25
|
+
> RFC 2812 (`SERVICE` / `SUMMON` / `USERS`) verbs are formally dropped.
|
|
26
|
+
>
|
|
27
|
+
> **IRCv3 extensions.** Negotiated caps include `account-notify`,
|
|
28
|
+
> `msgid`, `standard-replies`, `MONITOR`, `labeled-response`, `sts`,
|
|
29
|
+
> `draft/typing`, `draft/multiline`, `draft/read-marker`, and
|
|
30
|
+
> `draft/pre-away`, plus the ISUPPORT tokens for each and the
|
|
31
|
+
> read-only user mode `S` (TLS connected).
|
|
32
|
+
>
|
|
33
|
+
> **Web client.** A vendored **Kiwi IRC** SPA is served at
|
|
34
|
+
> `/webclient/` and a static landing page at `/` directly by the
|
|
35
|
+
> Cloudflare Worker, with **Cross-Site WebSocket Hijacking (CSWSH)
|
|
36
|
+
> defense** wired into the WS upgrade path (same-origin auto-derive by
|
|
37
|
+
> default, optional `WEB_ORIGINS` allowlist for cross-origin
|
|
38
|
+
> deploys). The `docs/` wiki is rendered to standalone HTML at
|
|
39
|
+
> `/docs/`.
|
|
40
|
+
>
|
|
41
|
+
> **Integrated IRC services.** NickServ, ChanServ, HostServ,
|
|
42
|
+
> OperServ, and MemoServ run inside the daemon (no separate services
|
|
43
|
+
> process, no S2S link) backed by a `ServicesStore` port with
|
|
44
|
+
> **persistent D1 (Cloudflare) and DynamoDB (AWS) backends**
|
|
45
|
+
> (write-behind, surviving redeploys). The SASL `AccountStore` and
|
|
46
|
+
> NickServ accounts are **unified** under one scrypt-hashed credential
|
|
47
|
+
> store, `PASS <nick>:<password>` identifies at or after registration,
|
|
48
|
+
> and `draft/read-marker` / `draft/pre-away` persist through the
|
|
49
|
+
> `ServicesStore`.
|
|
50
|
+
>
|
|
51
|
+
> See `CHANGELOG.md` for the per-release manifests.
|
|
51
52
|
|
|
52
53
|
---
|
|
53
54
|
|
|
@@ -299,23 +300,28 @@ credentials still go through the hashed SASL account store
|
|
|
299
300
|
(`HashedAccountStore` / D1 / DynamoDB), which is unaffected.
|
|
300
301
|
|
|
301
302
|
**PASS-based account login:** in addition to SASL, a client may identify
|
|
302
|
-
to its NickServ account
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
303
|
+
to its NickServ account by sending `PASS <nick>:<password>` (the same
|
|
304
|
+
`<nick>:<password>` literal the `SASL_ACCOUNTS` seed tooling uses).
|
|
305
|
+
When an `AccountStore` is configured, `NICK alice` + `PASS alice:hunter2`
|
|
306
|
+
+ `USER …` verifies the credentials against the account store and, on
|
|
307
|
+
success, logs the connection in as `alice` — emitting `900 RPL_LOGGEDIN`
|
|
308
|
+
+ `903 RPL_SASLSUCCESS` (the same "account is set" numerics SASL uses)
|
|
309
|
+
before `001 RPL_WELCOME`, stamping user mode `+r`, and running the same
|
|
310
|
+
read-marker / away / memo replay as a SASL login. The login is also
|
|
311
|
+
honoured when `PASS <nick>:<password>` arrives **after** registration
|
|
312
|
+
has completed (e.g. a client that sent `NICK`+`USER` before `PASS`, or
|
|
313
|
+
any `PASS` arriving after `CAP END` resolved): the same verify +
|
|
314
|
+
`applyAccountSuccess` pipeline runs, emitting `900`/`903` and stamping
|
|
315
|
+
`+r` in-band. The nick left of the `:` must match the connection's
|
|
316
|
+
current nick; a bare `PASS <value>` with no colon is always treated as
|
|
317
|
+
a server-password candidate (silent no-op once registration has
|
|
318
|
+
completed), never as account credentials. A wrong password or unknown
|
|
319
|
+
nick is indistinguishable (no `904`/`464` from the auth path) and the
|
|
320
|
+
connection simply proceeds un-identified when no server password is
|
|
321
|
+
set. This composes with the gate above by precedence: an
|
|
322
|
+
already-identified connection (SASL or PASS-auth) satisfies the gate;
|
|
323
|
+
a `<nick>:<password>` that fails verify never matches a bare shared
|
|
324
|
+
secret, so a server with both configured still rejects it with `464`.
|
|
319
325
|
|
|
320
326
|
### Connecting
|
|
321
327
|
|
|
@@ -386,7 +392,7 @@ against any line-oriented WebSocket endpoint.
|
|
|
386
392
|
|
|
387
393
|
## Load testing (`tools/load-test`)
|
|
388
394
|
|
|
389
|
-
A synthetic WebSocket IRC client pool
|
|
395
|
+
A synthetic WebSocket IRC client pool. It opens N
|
|
390
396
|
connections to a `ws://` / `wss://` target, registers each (NICK/USER),
|
|
391
397
|
joins a channel, and optionally chats. Per-stage **p50 / p95 / p99
|
|
392
398
|
latency** (connect / register / join / message) and the **drop rate**
|
|
@@ -448,12 +454,22 @@ Build the SPA + landing page (needs the submodule + `yarn`, provided by
|
|
|
448
454
|
|
|
449
455
|
```bash
|
|
450
456
|
git submodule update --init apps/web/upstream # one-time per clone
|
|
457
|
+
git submodule update --init docs # one-time per clone (docs site)
|
|
451
458
|
pnpm --filter web build:staging # or :prod / default
|
|
452
459
|
# → apps/web/dist/index.html (landing page, served at /)
|
|
453
460
|
# → apps/web/dist/webclient/index.html (Kiwi SPA, served at /webclient/)
|
|
454
461
|
# → apps/web/dist/webclient/static/config.json (baked, env-specific)
|
|
462
|
+
# → apps/web/dist/docs/<slug>.html (rendered docs/, served at /docs/)
|
|
455
463
|
```
|
|
456
464
|
|
|
465
|
+
The build also renders the `docs/` submodule (the Gitea wiki, 23 GFM
|
|
466
|
+
markdown files) to standalone HTML under `apps/web/dist/docs/` so the
|
|
467
|
+
existing `[assets]` binding serves them at `/docs/<slug>.html` (and
|
|
468
|
+
`/docs/` for the index — `Home.md` becomes `index.html`). Requires the
|
|
469
|
+
`docs/` submodule checkout; the build fails loudly with the recovery
|
|
470
|
+
command if it is missing or empty (mirroring the Kiwi `upstream/` guard).
|
|
471
|
+
`Home.md` is the docs landing page; `ADR-Index.md` lists every ADR.
|
|
472
|
+
|
|
457
473
|
Then run everything locally (Worker serves SPA + landing page + WS on
|
|
458
474
|
one origin):
|
|
459
475
|
|
|
@@ -462,6 +478,7 @@ pnpm build # workspace packages
|
|
|
462
478
|
pnpm --filter @serverless-ircd/cf-worker dev # http://localhost:8787
|
|
463
479
|
# / → landing page
|
|
464
480
|
# /webclient/ → Kiwi SPA (opens ws://localhost:8787/)
|
|
481
|
+
# /docs/ → rendered docs/ (Home → index, per-page HTML)
|
|
465
482
|
# /health → plaintext liveness
|
|
466
483
|
```
|
|
467
484
|
|
|
@@ -552,92 +569,33 @@ port and an `IdFactory` port are injected so tests are fully deterministic.
|
|
|
552
569
|
|
|
553
570
|
## Roadmap
|
|
554
571
|
|
|
555
|
-
A v1 ships when
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
- **
|
|
567
|
-
|
|
568
|
-
- **
|
|
569
|
-
|
|
570
|
-
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
mTLS → SASL `EXTERNAL`, transport-parametrized contract suite, ADR-009.
|
|
579
|
-
- **Load testing & compatibility sweep.** 🔄 partial as of
|
|
580
|
-
**v0.6.0**: the `tools/load-test` 10k-connection harness is in-tree;
|
|
581
|
-
the formal 10k-connection report per platform and the client
|
|
582
|
-
compatibility matrix (WeeChat / HexChat / IRCCloud / TheLounge) are
|
|
583
|
-
still pending.
|
|
584
|
-
- **PLAN-FIXES remediation.** ✅ landed across v0.3.0–v0.4.0
|
|
585
|
-
(MessageStore wired into all three adapters, AccountStore end-to-end,
|
|
586
|
-
the DynamoDB `Accounts` table decided, CF enumeration RPCs, AWS QUIT
|
|
587
|
-
fanout, `OPER` / `TAGMSG` / `WHOWAS`, RFC 1459 case-mapping, real
|
|
588
|
-
MOTD, config-driven server identity, AWS max-clients admission,
|
|
589
|
-
CF D1 SASL store, and a sweep of dead-code / stale-comment cleanup).
|
|
590
|
-
- **Runtime & tooling baseline.** ✅ landed in **v0.4.0**
|
|
591
|
-
(Node ≥ 24 / pnpm 11).
|
|
592
|
-
- **IRCv3 WebSocket extension.** ✅ landed in **v0.4.0**:
|
|
593
|
-
`text.ircv3.net` / `binary.ircv3.net` subprotocol negotiation +
|
|
594
|
-
per-message framing across the CF Worker, AWS APIGW, local CLI, and
|
|
595
|
-
`tcp-ws-forwarder` (510-byte budget, `1009`/`1003` close codes,
|
|
596
|
-
legacy fallback).
|
|
597
|
-
- **Deferred IRC verbs.** ✅ landed in **v0.5.0**: `KILL`,
|
|
598
|
-
`REHASH`, `LUSERS` + `STATS`, `TRACE`, `WALLOPS`, `SETNAME`, plus
|
|
599
|
-
the formal drop of the S2S (`CONNECT`/`SQUIT`/`LINKS`) and obsolete
|
|
600
|
-
RFC 2812 (`SERVICE`/`SUMMON`/`USERS`) verbs. The protocol surface is
|
|
601
|
-
feature-complete for modern clients.
|
|
602
|
-
- **IRCv3 extension sweep.** ✅ landed in **v0.6.0**: ten
|
|
603
|
-
negotiated caps (`account-notify`, `msgid`, `standard-replies`,
|
|
604
|
-
`MONITOR`, `labeled-response`, `sts`, `draft/typing`,
|
|
605
|
-
`draft/multiline`, `draft/read-marker`, `draft/pre-away`), the
|
|
606
|
-
ISUPPORT tokens for the new caps (`MONITOR`/`MULTILINE`/`TYPING`/
|
|
607
|
-
`STATUSMSG`/`EXTBAN`/`ACCOUNTEXTBAN`), the read-only user mode `S`
|
|
608
|
-
(TLS connected), and the async push fanout seam (Registry hooks →
|
|
609
|
-
`MONITOR` online/offline, `BroadcastWallops`-style caps-gated
|
|
610
|
-
delivery).
|
|
611
|
-
- **IRC services.** ✅ landed in **v0.8.0**: `ServicesStore` port
|
|
612
|
-
+ integrated NickServ (`REGISTER`/`IDENTIFY`/`DROP`/`INFO`/`SET
|
|
613
|
-
ENFORCE` + nick enforcement), ChanServ (`REGISTER`/`DROP`/`SET`/
|
|
614
|
-
`ACCESS`/`LEVELS` + `+r`/`+R`/`+M`), HostServ
|
|
615
|
-
(`ON`/`OFF`/`REQUEST` + oper `SET`/`APPROVE`), OperServ
|
|
616
|
-
(`AKILL`/`JUPE`/`RAW`), MemoServ (`SEND`/`LIST`/`READ`/`DEL`).
|
|
617
|
-
Persistent backends ship alongside — **D1** on Cloudflare,
|
|
618
|
-
**DynamoDB** on AWS — both write-behind so registrations survive
|
|
619
|
-
redeploys. The SASL `AccountStore` and NickServ accounts are
|
|
620
|
-
**unified** under one scrypt-hashed credential store, `PASS
|
|
621
|
-
<nick>:<password>` identifies at registration, and `draft/read-marker`
|
|
622
|
-
/ `draft/pre-away` now persist through the `ServicesStore` (no longer
|
|
623
|
-
in-memory-only). See `docs/Services.md`.
|
|
624
|
-
- **Web client.** 🔄 partial as of **v0.7.0**: a vendored
|
|
625
|
-
Kiwi IRC SPA is served at `/webclient/` and a static landing page at `/`
|
|
626
|
-
by the Cloudflare Worker via an `[assets]` binding, with a Zod-
|
|
627
|
-
validated per-env config matrix and **CSWSH defense** (same-origin
|
|
628
|
-
auto-derive + optional `WEB_ORIGINS` allowlist). The Playwright
|
|
629
|
-
headless-browser e2e is the remaining open ticket.
|
|
630
|
-
See `docs/WebClientGuide.md` for the contributor/operator guide.
|
|
631
|
-
- **Coverage hardening.** 🔄 partial as of **v0.7.0**: `aws-adapter`
|
|
632
|
-
and `aws-stack` clear the 90% gate (v0.6.0); the CF packages run
|
|
633
|
-
under istanbul (v0.6.0). The remaining packages (`cf-adapter`,
|
|
634
|
-
`local-cli`, `load-test`, `cf-tcp-container`, `tcp-ws-forwarder`,
|
|
635
|
-
`irc-test-support`, `web`) are **above the gate but below 100%** —
|
|
636
|
-
follow-ups drive each to 100%.
|
|
572
|
+
A v1 ships when the parametrized contract suite passes against every
|
|
573
|
+
runtime, the client compatibility matrix is signed off, and every
|
|
574
|
+
package clears its coverage gate. Per-release manifests live in
|
|
575
|
+
`CHANGELOG.md`.
|
|
576
|
+
|
|
577
|
+
Active follow-ups:
|
|
578
|
+
|
|
579
|
+
- **Client compatibility sweep** — formal sign-off on ≥3 reference
|
|
580
|
+
clients (WeeChat / HexChat / IRCCloud / TheLounge) against both
|
|
581
|
+
deployed adapters, on both the WebSocket and `irc+tls :6697`
|
|
582
|
+
surfaces.
|
|
583
|
+
- **Web client e2e** — Playwright headless-browser e2e exercising the
|
|
584
|
+
vendored Kiwi IRC SPA against a deployed stack.
|
|
585
|
+
- **Coverage hardening** — `aws-adapter` and `aws-stack` clear the 90%
|
|
586
|
+
gate; the CF packages run under istanbul. The remaining packages
|
|
587
|
+
(`cf-adapter`, `local-cli`, `load-test`, `cf-tcp-container`,
|
|
588
|
+
`tcp-ws-forwarder`, `irc-test-support`, `web`) sit above the gate
|
|
589
|
+
but below 100%; follow-ups drive each to full coverage. `irc-core`,
|
|
590
|
+
`irc-server`, and `in-memory-runtime` are at 100%.
|
|
591
|
+
- **Persistent ChanServ ban list** — ban masks currently live on
|
|
592
|
+
`ChannelState.banMasks` and do not survive an empty-recreate of a
|
|
593
|
+
channel. Extending `ServicesStore` with a persistent ban list is the
|
|
594
|
+
documented next step.
|
|
637
595
|
|
|
638
596
|
---
|
|
639
597
|
|
|
640
|
-
## Protocol scope
|
|
598
|
+
## Protocol scope
|
|
641
599
|
|
|
642
600
|
**Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
|
|
643
601
|
`PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
|
|
@@ -654,9 +612,9 @@ disconnect), `REHASH` (config reload via `IrcRuntime.reloadConfig()`),
|
|
|
654
612
|
`WALLOPS` (global broadcast to every `+w` user).
|
|
655
613
|
|
|
656
614
|
(Out of scope: the S2S verbs `CONNECT`/`SQUIT`/`LINKS` and the obsolete
|
|
657
|
-
RFC 2812 verbs `SERVICE`/`SUMMON`/`USERS` are **formally dropped**
|
|
658
|
-
|
|
659
|
-
|
|
615
|
+
RFC 2812 verbs `SERVICE`/`SUMMON`/`USERS` are **formally dropped** —
|
|
616
|
+
they return `421 ERR_UNKNOWNCOMMAND`, and their reserved numerics have
|
|
617
|
+
been removed. S2S linking is a stated non-goal.)
|
|
660
618
|
|
|
661
619
|
**Channel modes:** `o v b i k l t n m s p`, plus the
|
|
662
620
|
services-derived modes `r` (registered), `R` (block unidentified
|
|
@@ -668,24 +626,45 @@ via `276 RPL_WHOISSECURE`, not settable via `MODE`) and the read-only
|
|
|
668
626
|
`r` (registered — set by NickServ `IDENTIFY` / SASL login, surfaced as
|
|
669
627
|
`+r`, not settable via `MODE`).
|
|
670
628
|
|
|
671
|
-
**Integrated IRC services
|
|
672
|
-
`
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
`
|
|
629
|
+
**Integrated IRC services:** the service nicks `NickServ`, `ChanServ`,
|
|
630
|
+
`HostServ`, `OperServ`, and `MemoServ` are reserved and routed to
|
|
631
|
+
dedicated pure reducers when a `ServicesStore` is bound — there is **no
|
|
632
|
+
separate services process and no S2S link**. Each service also accepts a
|
|
633
|
+
**shortcut verb** (`/NICKSERV`, `/NS`, `/CS`, `/HS`, `/MS`, `/OS`, …)
|
|
634
|
+
that the daemon rewrites to the equivalent `PRIVMSG <Service> :<args>`
|
|
635
|
+
for clients with a dedicated slash-command UX. Reached via `PRIVMSG
|
|
636
|
+
<Service> :<subcommand>`:
|
|
637
|
+
|
|
638
|
+
- **NickServ** — `REGISTER` / `IDENTIFY` (alias: `ID`) / `DROP` / `INFO`
|
|
639
|
+
(owner + oper only for the `Email:` line) / `SET ENFORCE` + nick
|
|
640
|
+
enforcement on the `NICK` path. `INFO <nick>` with no target resolves
|
|
641
|
+
to the caller's own account.
|
|
642
|
+
- **ChanServ** — `REGISTER` / `DROP` / `SET` (`FOUNDER` / `MLOCK` /
|
|
643
|
+
`RESTRICTED` / `KEEPTOPIC`) / `INFO` / `ACCESS` (`SOP` / `AOP` / `HOP`
|
|
644
|
+
/ `VOP`, each `ADD` / `DEL` / `LIST`) / `LEVELS`, plus the prefix /
|
|
645
|
+
roster mutation verbs `OP`, `DEOP`, `VOICE`, `DEVOICE`, `KICK`, and
|
|
646
|
+
`BAN` / `UNBAN` (each emits the corresponding `:ChanServ MODE` /
|
|
647
|
+
`KICK` broadcast and `ApplyChannelDelta` against the channel
|
|
648
|
+
authority). Auto-op / auto-voice on `JOIN` follows the access list;
|
|
649
|
+
the first joiner of a registered channel no longer gets auto-opped
|
|
650
|
+
merely for being first.
|
|
651
|
+
- **HostServ** — `ON` / `OFF` / `REQUEST` + oper `SET` / `APPROVE` /
|
|
652
|
+
`ACTIVATE` / `REJECT` / `LIST`, with `CHGHOST` fanout. An assigned
|
|
653
|
+
vhost is **auto-applied on identify** (SASL, NickServ `IDENTIFY`, or
|
|
654
|
+
PASS-auth): `state.host` is set, `state.vhostActive` is stamped, and
|
|
655
|
+
any currently-joined `chghost`-capable peers see the broadcast.
|
|
656
|
+
Oper-only subcommands are hidden from non-opers in the help NOTICE.
|
|
657
|
+
- **OperServ** — `AKILL` / `JUPE` / `UNJUPE` / `RAW` (oper-only).
|
|
658
|
+
- **MemoServ** — `SEND` / `LIST` / `READ` / `DEL` with queue delivery
|
|
659
|
+
at identify.
|
|
660
|
+
|
|
661
|
+
The SASL `AccountStore` and NickServ accounts share one scrypt-hashed
|
|
662
|
+
credential store, so a registered nick is also a SASL login and vice
|
|
663
|
+
versa. Backends: D1 on Cloudflare, DynamoDB on AWS, in-memory for the
|
|
664
|
+
local CLI / tests (all write-behind; registrations survive redeploys).
|
|
665
|
+
When no store is bound, services commands reply `501` and the rest of
|
|
666
|
+
the daemon is unaffected. See `docs/Services.md` for the full
|
|
667
|
+
reference.
|
|
689
668
|
|
|
690
669
|
**IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
|
|
691
670
|
`TAGMSG` command), `server-time`, `account-tag`, `account-notify`
|
|
@@ -724,42 +703,23 @@ bridges a stock TCP client to a deployed WebSocket endpoint.
|
|
|
724
703
|
|
|
725
704
|
## Further reading
|
|
726
705
|
|
|
727
|
-
- `CHANGELOG.md` — per-release manifests
|
|
728
|
-
the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
|
|
729
|
-
gates), v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
|
|
730
|
-
ChannelRegistryDO, SASL account persistence), v0.4.0 (dual transport
|
|
731
|
-
`irc+tls :6697`, SASL `EXTERNAL`/mTLS, WHOWAS + remaining verbs, D1 account
|
|
732
|
-
store, Node 24/pnpm 11), v0.5.0 (deferred IRC verbs `KILL` / `REHASH`
|
|
733
|
-
/ `LUSERS` + `STATS` / `TRACE` / `WALLOPS` / `SETNAME`, plus the formal
|
|
734
|
-
drop of the S2S and obsolete RFC 2812 verbs), v0.6.0 (the IRCv3
|
|
735
|
-
extension sweep — `account-notify`, `msgid`, `standard-replies`,
|
|
736
|
-
`MONITOR`, `labeled-response`, `sts`, `draft/typing`, `draft/multiline`,
|
|
737
|
-
`draft/read-marker`, `draft/pre-away`, plus user mode `S`; the
|
|
738
|
-
required-`serverName` gate; `tools/load-test`; aws-adapter / aws-stack
|
|
739
|
-
coverage unblocks; CF packages moved to istanbul; deploys now
|
|
740
|
-
manual-only), and **v0.7.0** (the web client — vendored Kiwi IRC SPA
|
|
741
|
-
at `/webclient/`, static landing page at `/`, Cloudflare Worker
|
|
742
|
-
`[assets]` binding, per-env Zod-validated config matrix, and CSWSH
|
|
743
|
-
defense via same-origin auto-derive + optional `WEB_ORIGINS`;
|
|
744
|
-
`/health` becomes an explicit route; mutation-killing tests for
|
|
745
|
-
`irc-core`'s parser / batch / numerics), and **v0.8.0** (integrated
|
|
746
|
-
IRC services — NickServ / ChanServ / HostServ / OperServ / MemoServ
|
|
747
|
-
backed by a `ServicesStore` port with persistent D1 + DynamoDB
|
|
748
|
-
backends; unified SASL + NickServ scrypt-hashed account store;
|
|
749
|
-
`PASS <nick>:<password>` registration login; the `draft/read-marker`
|
|
750
|
-
`MARKREAD` verb).
|
|
706
|
+
- `CHANGELOG.md` — per-release manifests in Keep a Changelog format.
|
|
751
707
|
- `docs/Services.md` — operator and contributor reference for the
|
|
752
708
|
integrated IRC services: architecture (integrated vs. pseudo-client),
|
|
753
|
-
the per-service command tables
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
-
|
|
764
|
-
|
|
765
|
-
|
|
709
|
+
the per-service command tables (including ChanServ `OP`/`DEOP`/
|
|
710
|
+
`VOICE`/`DEVOICE`/`KICK`/`BAN`/`UNBAN`, HostServ oper approval flow,
|
|
711
|
+
the `+r`/`+R`/`+M` modes, nick enforcement, the unified scrypt
|
|
712
|
+
account store, service shortcut verbs), adapter backends, and an
|
|
713
|
+
end-to-end registration walkthrough.
|
|
714
|
+
- `docs/WebClientGuide.md` — end-to-end contributor/operator doc for
|
|
715
|
+
the web client: build pipeline, per-env config matrix, CSWSH
|
|
716
|
+
rationale and the optional `WEB_ORIGINS` var, local dev, optional
|
|
717
|
+
Cloudflare Pages alternative, and troubleshooting.
|
|
718
|
+
- `docs/Cloudflare-TCP-Deployment.md` and `docs/AWS-TCP-Deployment.md`
|
|
719
|
+
— end-to-end guides for the `:6697` TCP+TLS variants
|
|
720
|
+
(Spectrum/Container on CF, NLB + Lambda streaming on AWS), including
|
|
721
|
+
mTLS trust-store setup.
|
|
722
|
+
- `README.md` in each `packages/*` and `apps/*` — per-package notes
|
|
723
|
+
(e.g. `packages/aws-adapter/README.md` for the DynamoDB-Local test
|
|
724
|
+
setup, `apps/aws-stack/README.md` for CDK commands and localstack
|
|
725
|
+
validation).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/cf-tcp-container",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -95,6 +95,9 @@ interface ConnectionBindings {
|
|
|
95
95
|
const DEFAULT_ID_FACTORY = {
|
|
96
96
|
batchId: () => randomUUID(),
|
|
97
97
|
nonce: () => randomUUID(),
|
|
98
|
+
// `sessionId` is required for `IdFactory` interface completeness; no
|
|
99
|
+
// current code path invokes it (only batchId/nonce/traceId are called).
|
|
100
|
+
// c8 ignore next 1
|
|
98
101
|
sessionId: () => randomUUID(),
|
|
99
102
|
traceId: () => randomUUID(),
|
|
100
103
|
};
|
|
@@ -154,12 +157,21 @@ export async function startContainerServer(opts: StartContainerOptions): Promise
|
|
|
154
157
|
const tcpServer = createServer((socket) => {
|
|
155
158
|
sockets.add(socket);
|
|
156
159
|
|
|
160
|
+
// `remoteAddress` is always set on a TCP server's connection socket;
|
|
161
|
+
// the 'unknown' fallback is defensive and unreachable in practice.
|
|
162
|
+
/* c8 ignore start */
|
|
157
163
|
const ip = socket.remoteAddress ?? 'unknown';
|
|
164
|
+
/* c8 ignore stop */
|
|
158
165
|
const decision = runtime.admitConnection(ip, undefined);
|
|
159
166
|
if (!decision.ok) {
|
|
167
|
+
// A freshly-accepted TCP socket is always writable; the else branch
|
|
168
|
+
// (skip the ERROR write) is a defensive guard against an impossible
|
|
169
|
+
// race and cannot be exercised over a real socket.
|
|
170
|
+
/* c8 ignore start */
|
|
160
171
|
if (socket.writable) {
|
|
161
172
|
socket.write(`ERROR :Closing link: (connection limit reached: ${decision.reason})\r\n`);
|
|
162
173
|
}
|
|
174
|
+
/* c8 ignore stop */
|
|
163
175
|
socket.destroy();
|
|
164
176
|
sockets.delete(socket);
|
|
165
177
|
return;
|
|
@@ -213,8 +225,12 @@ export async function startContainerServer(opts: StartContainerOptions): Promise
|
|
|
213
225
|
const onListening = (): void => {
|
|
214
226
|
tcpServer.removeListener('error', onError);
|
|
215
227
|
const addr = tcpServer.address();
|
|
216
|
-
//
|
|
228
|
+
// Inside the 'listening' handler `address()` is always an AddressInfo
|
|
229
|
+
// (TCP server, just bound); the null/string returns only occur for IPC
|
|
230
|
+
// sockets or before listening. The `opts.port` fallback is unreachable.
|
|
231
|
+
/* c8 ignore start */
|
|
217
232
|
const port = typeof addr === 'object' && addr !== null ? addr.port : opts.port;
|
|
233
|
+
/* c8 ignore stop */
|
|
218
234
|
resolve({
|
|
219
235
|
port,
|
|
220
236
|
host,
|
|
@@ -255,9 +271,13 @@ function attachConnection(
|
|
|
255
271
|
): ConnectionBindings {
|
|
256
272
|
const id = randomUUID();
|
|
257
273
|
const state = createConnection({ id, connectedSince: clock.now() });
|
|
274
|
+
// `transport.sourceHost` is always supplied (set to the peer IP) by the
|
|
275
|
+
// only call site above; the else branch is defensive and unreachable.
|
|
276
|
+
/* c8 ignore start */
|
|
258
277
|
if (transport.sourceHost !== undefined) {
|
|
259
278
|
state.host = transport.sourceHost;
|
|
260
279
|
}
|
|
280
|
+
/* c8 ignore stop */
|
|
261
281
|
|
|
262
282
|
const logger: Logger = new ConsoleLogger({ connectionId: id }, undefined, LogLevel.Info);
|
|
263
283
|
|
|
@@ -161,6 +161,49 @@ describe('config-loader — container-specific knobs', () => {
|
|
|
161
161
|
expect(cfg.server.operCreds).toEqual([{ user: 'admin', password: 'secret' }]);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
it('exercises the OPER_PASSWORD ?? "" default then rejects partial OPER_USER-only creds', () => {
|
|
165
|
+
// The `?? ''` default for the missing OPER_PASSWORD is exercised, but
|
|
166
|
+
// parseServerConfig then rejects the empty password — partial oper
|
|
167
|
+
// creds are a misconfiguration, not a silently-accepted one.
|
|
168
|
+
expect(() =>
|
|
169
|
+
loadContainerConfig({
|
|
170
|
+
SERVER_NAME: 'irc.test',
|
|
171
|
+
NETWORK_NAME: 'TestNet',
|
|
172
|
+
OPER_USER: 'admin',
|
|
173
|
+
}),
|
|
174
|
+
).toThrowError(/password/u);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('exercises the OPER_USER ?? "" default then rejects partial OPER_PASSWORD-only creds', () => {
|
|
178
|
+
expect(() =>
|
|
179
|
+
loadContainerConfig({
|
|
180
|
+
SERVER_NAME: 'irc.test',
|
|
181
|
+
NETWORK_NAME: 'TestNet',
|
|
182
|
+
OPER_PASSWORD: 'secret',
|
|
183
|
+
}),
|
|
184
|
+
).toThrowError(/user/u);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('treats an overflow-numeric CREATED_AT (Infinity) as a verbatim string', () => {
|
|
188
|
+
const cfg = loadContainerConfig({
|
|
189
|
+
SERVER_NAME: 'irc.test',
|
|
190
|
+
NETWORK_NAME: 'TestNet',
|
|
191
|
+
// 400 nines overflows Number.MAX_VALUE → Number() yields Infinity,
|
|
192
|
+
// which is not finite, so parseCreatedAt falls through to the raw string.
|
|
193
|
+
CREATED_AT: '9'.repeat(400),
|
|
194
|
+
});
|
|
195
|
+
expect(cfg.server.createdAt).toBe('9'.repeat(400));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('skips a SASL line whose username is set but password is empty', () => {
|
|
199
|
+
const cfg = loadContainerConfig({
|
|
200
|
+
SERVER_NAME: 'irc.test',
|
|
201
|
+
NETWORK_NAME: 'TestNet',
|
|
202
|
+
SASL_ACCOUNTS: 'alice:\nbob:valid',
|
|
203
|
+
});
|
|
204
|
+
expect(cfg.server.saslAccounts).toEqual([{ username: 'bob', password: 'valid' }]);
|
|
205
|
+
});
|
|
206
|
+
|
|
164
207
|
it('skips malformed SASL account lines', () => {
|
|
165
208
|
const cfg = loadContainerConfig({
|
|
166
209
|
SERVER_NAME: 'irc.test',
|