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
package/README.md
CHANGED
|
@@ -6,20 +6,33 @@ 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.6.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
|
-
>
|
|
17
|
+
> path, plus **SASL `EXTERNAL` via mTLS** and a **Cloudflare D1-backed
|
|
18
|
+
> SASL account store**. v0.5.0 completed 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. **v0.6.0 lands the IRCv3 extension sweep
|
|
23
|
+
> (Phase 12)** — ten caps (`account-notify`, `msgid`,
|
|
24
|
+
> `standard-replies`, `MONITOR`, `labeled-response`, `sts`,
|
|
25
|
+
> `draft/typing`, `draft/multiline`, `draft/read-marker`,
|
|
26
|
+
> `draft/pre-away`), the ISUPPORT tokens for the new caps, and the
|
|
27
|
+
> read-only user mode `S` (TLS connected) — closes the
|
|
28
|
+
> required-`serverName` config gate, ships a `tools/load-test`
|
|
29
|
+
> 10k-connection harness, and lifts `aws-adapter` / `aws-stack` over
|
|
30
|
+
> the 90% coverage gate (CF packages moved to istanbul coverage,
|
|
31
|
+
> clearing the v0.5.0 workerd-incompatibility caveat). Remaining 0.x
|
|
32
|
+
> work: the client compatibility sweep (WeeChat / HexChat / IRCCloud /
|
|
33
|
+
> TheLounge), IRC services (NickServ / ChanServ / HostServ /
|
|
34
|
+
> OperServ / MemoServ), a web client (vendored Kiwi IRC), and driving
|
|
35
|
+
> the remaining sub-100% packages to full coverage. See
|
|
23
36
|
> `CHANGELOG.md` for the per-release manifests.
|
|
24
37
|
|
|
25
38
|
---
|
|
@@ -127,6 +140,7 @@ ServerlessIRCd/
|
|
|
127
140
|
│ └── local-cli/ runnable WS + TCP server using in-memory-runtime
|
|
128
141
|
├── tools/
|
|
129
142
|
│ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
|
|
143
|
+
│ ├── load-test/ synthetic WebSocket IRC client farm (10k conns, p50/p95/p99, drop rate)
|
|
130
144
|
│ ├── ci-hardening/ coverage-gate + mutation-config validators
|
|
131
145
|
│ ├── seed-aws-accounts.ts scrypt-hash SASL PLAIN accounts into DynamoDB
|
|
132
146
|
│ └── seed-cf-accounts.ts scrypt-hash SASL PLAIN accounts into Cloudflare D1
|
|
@@ -227,6 +241,8 @@ On startup it logs a JSON line like:
|
|
|
227
241
|
| `--tcp-port <n>` | `<port> + 1` | TCP port for RFC-style IRC clients. |
|
|
228
242
|
| `--no-tcp` | | Disable the TCP listener (WebSocket only). |
|
|
229
243
|
| `--motd-file <p>` | built-in | Read MOTD lines from this file (one per line). |
|
|
244
|
+
| `--server-name <h>`| `irc.localhost` | Server hostname advertised in `001`/`005`. Production MUST override. |
|
|
245
|
+
| `--network-name <n>`| `LocalNet` | Network name advertised in `005 NETWORK=…`. |
|
|
230
246
|
| `-h, --help` | | Show help and exit. |
|
|
231
247
|
|
|
232
248
|
Pass `--host 0.0.0.0` to expose the server on all interfaces. `SIGINT` /
|
|
@@ -299,6 +315,91 @@ against any line-oriented WebSocket endpoint.
|
|
|
299
315
|
|
|
300
316
|
---
|
|
301
317
|
|
|
318
|
+
## Load testing (`tools/load-test`)
|
|
319
|
+
|
|
320
|
+
A synthetic WebSocket IRC client farm (PLAN §7, Phase 7). It opens N
|
|
321
|
+
connections to a `ws://` / `wss://` target, registers each (NICK/USER),
|
|
322
|
+
joins a channel, and optionally chats. Per-phase **p50 / p95 / p99
|
|
323
|
+
latency** (connect / register / join / message) and the **drop rate**
|
|
324
|
+
are captured, then printed as a markdown report to stdout (and written
|
|
325
|
+
to `--report <path>` when given).
|
|
326
|
+
|
|
327
|
+
Build the workspace, then start a run (from the repo root):
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
pnpm build
|
|
331
|
+
|
|
332
|
+
pnpm --filter load-test start -- \
|
|
333
|
+
--target wss://irc.staging.example.com/ \
|
|
334
|
+
--connections 10000 --concurrency 200 --ramp-ms 60000 \
|
|
335
|
+
--channel '#loadtest' --messages 3 --platform cf-staging \
|
|
336
|
+
--report reports/cf-staging-2026-08-03.md
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The harness drives the WebSocket transport only (the serverless default);
|
|
340
|
+
a TCP IRC client is not what a serverless load test exercises. Each
|
|
341
|
+
connection is independent — the package ships no `@serverless-ircd/*`
|
|
342
|
+
runtime dependency (only the IRC line framing shared with the
|
|
343
|
+
forwarder), so it can be pointed at any WebSocket IRC endpoint.
|
|
344
|
+
|
|
345
|
+
### CLI flags
|
|
346
|
+
|
|
347
|
+
| Flag | Default | Description |
|
|
348
|
+
|-------------------------|-------------|----------------------------------------------------------|
|
|
349
|
+
| `--target <url>` | (required) | Upstream `ws://` or `wss://` URL to load test. |
|
|
350
|
+
| `--connections <n>` | `1000` | Total connections to open. |
|
|
351
|
+
| `--concurrency <n>` | `50` | Concurrent in-flight connection attempts during ramp-up. |
|
|
352
|
+
| `--ramp-ms <ms>` | `30000` | Ramp-up duration over which connections are opened. |
|
|
353
|
+
| `--channel <c>` | `#loadtest` | Channel every client JOINs. |
|
|
354
|
+
| `--messages <n>` | `0` | PRIVMSGs each client sends after JOIN (`0` = connect only). |
|
|
355
|
+
| `--echo-message` | | Negotiate `echo-message` so sent PRIVMSGs round-trip. |
|
|
356
|
+
| `--platform <name>` | `unknown` | Label baked into the report header. |
|
|
357
|
+
| `--report <path>` | | Write the markdown report to this path. |
|
|
358
|
+
| `-h, --help` | | Show help and exit. |
|
|
359
|
+
|
|
360
|
+
`SIGINT` / `SIGTERM` cancel an in-flight run gracefully (drains live
|
|
361
|
+
sockets, prints the partial report). The package ships its own vitest
|
|
362
|
+
suite at ≥90% coverage (config parsing, framing, harness, metrics,
|
|
363
|
+
report).
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## WebSocket transport (IRCv3 subprotocols)
|
|
368
|
+
|
|
369
|
+
The WebSocket entry points (the local CLI, the Cloudflare Worker, and the
|
|
370
|
+
AWS API Gateway `$connect` route) implement the
|
|
371
|
+
[IRCv3 WebSocket support](https://ircv3.net/specs/websocket.html) subprotocol
|
|
372
|
+
negotiation. A client advertises support by offering one of two registered
|
|
373
|
+
subprotocols in its opening handshake's `Sec-WebSocket-Protocol` header:
|
|
374
|
+
|
|
375
|
+
| Subprotocol | Frames | Notes |
|
|
376
|
+
|--------------------|----------|--------------------------------------------------|
|
|
377
|
+
| `text.ircv3.net` | UTF-8 text | Lone surrogates → U+FFFD; binary frames rejected (close `1003`). |
|
|
378
|
+
| `binary.ircv3.net` | binary | UTF-8 encoded bytes; same framing as `text`. |
|
|
379
|
+
|
|
380
|
+
The server echoes back the first supported entry in client-preference order
|
|
381
|
+
(in the `101` response's `Sec-WebSocket-Protocol` header). Once negotiated,
|
|
382
|
+
the connection uses **spec framing**:
|
|
383
|
+
|
|
384
|
+
- **One IRC message per WebSocket message** — a frame is never split on an
|
|
385
|
+
embedded line break. A single optional trailing `CR-LF` (or bare `LF`) is
|
|
386
|
+
stripped; the remainder is exactly one IRC line.
|
|
387
|
+
- **No trailing `CR-LF` on the wire**, outbound or inbound.
|
|
388
|
+
- **510-byte message budget** — the 512-byte IRC line limit minus the omitted
|
|
389
|
+
`CR-LF`. A message exceeding 510 bytes is a protocol violation and the
|
|
390
|
+
connection is closed with RFC 6455 code `1009` (Message Too Big).
|
|
391
|
+
|
|
392
|
+
**Legacy fallback.** A client that offers no recognized subprotocol (or none
|
|
393
|
+
at all) is still accepted, in **legacy mode**: frames are split on `\r\n` so
|
|
394
|
+
older clients that concatenate several messages into one frame keep working,
|
|
395
|
+
and the parser's existing 512-byte line cap applies. Nothing breaks until a
|
|
396
|
+
client opts into a subprotocol. The in-tree
|
|
397
|
+
[`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder)
|
|
398
|
+
offers `binary.ircv3.net` upstream, so a stock TCP IRC client bridged to a
|
|
399
|
+
deployed stack exercises the spec path end-to-end.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
302
403
|
## Testing strategy
|
|
303
404
|
|
|
304
405
|
This project follows strict TDD (Red → Green → Refactor) — every reducer is
|
|
@@ -334,20 +435,63 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
|
334
435
|
- **Phase 2** — `IrcRuntime` port, in-memory runtime, `ConnectionActor`,
|
|
335
436
|
local CLI, parametrized contract suite. ✅ landed in **v0.1.0**.
|
|
336
437
|
- **Phase 3** — Cloudflare adapter (ConnectionDO / ChannelDO / RegistryDO).
|
|
337
|
-
✅ landed in **v0.1.0** (staging
|
|
438
|
+
✅ landed in **v0.1.0** (staging deploy via CI; prod deploy is manual).
|
|
338
439
|
- **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
|
|
339
|
-
✅ landed in **v0.2.0** (staging
|
|
440
|
+
✅ landed in **v0.2.0** (staging deploy via CI; prod deploy is manual).
|
|
340
441
|
- **Phase 5** — Observability, security hardening, config, CI gates.
|
|
341
442
|
✅ landed across v0.2.0–v0.4.0 (logger port, server-password, cloaking,
|
|
342
443
|
admission limits, coverage + mutation gates, OPER credential auth, the
|
|
343
|
-
Cloudflare config loader, and AWS max-clients admission)
|
|
444
|
+
Cloudflare config loader, and AWS max-clients admission); the
|
|
445
|
+
required-`serverName` gate closed in **v0.6.0**.
|
|
344
446
|
- **Phase 6** — TLS hardening & raw TCP transport (`irc+tls :6697`).
|
|
345
447
|
✅ landed in **v0.4.0**: generalized `ConnectionActor` transport seam,
|
|
346
448
|
Cloudflare Spectrum + Container origin, AWS NLB + Lambda streaming,
|
|
347
449
|
mTLS → SASL `EXTERNAL`, transport-parametrized contract suite, ADR-009.
|
|
348
|
-
- **Phase 7
|
|
349
|
-
|
|
350
|
-
|
|
450
|
+
- **Phase 7 — Load testing & compatibility sweep.** 🔄 partial as of
|
|
451
|
+
**v0.6.0**: the `tools/load-test` 10k-connection harness is in-tree;
|
|
452
|
+
the formal 10k-connection report per platform and the client
|
|
453
|
+
compatibility matrix (WeeChat / HexChat / IRCCloud / TheLounge) are
|
|
454
|
+
still pending.
|
|
455
|
+
- **Phase 8 — PLAN-FIXES remediation.** ✅ landed across v0.3.0–v0.4.0
|
|
456
|
+
(MessageStore wired into all three adapters, AccountStore end-to-end,
|
|
457
|
+
the DynamoDB `Accounts` table decided, CF enumeration RPCs, AWS QUIT
|
|
458
|
+
fanout, `OPER` / `TAGMSG` / `WHOWAS`, RFC 1459 case-mapping, real
|
|
459
|
+
MOTD, config-driven server identity, AWS max-clients admission,
|
|
460
|
+
CF D1 SASL store, and a sweep of dead-code / stale-comment cleanup).
|
|
461
|
+
- **Phase 9 — Runtime & tooling baseline.** ✅ landed in **v0.4.0**
|
|
462
|
+
(Node ≥ 24 / pnpm 11).
|
|
463
|
+
- **Phase 10 — IRCv3 WebSocket extension.** ✅ landed in **v0.4.0**:
|
|
464
|
+
`text.ircv3.net` / `binary.ircv3.net` subprotocol negotiation +
|
|
465
|
+
per-message framing across the CF Worker, AWS APIGW, local CLI, and
|
|
466
|
+
`tcp-ws-forwarder` (510-byte budget, `1009`/`1003` close codes,
|
|
467
|
+
legacy fallback).
|
|
468
|
+
- **Phase 11 — Deferred IRC verbs.** ✅ landed in **v0.5.0**: `KILL`,
|
|
469
|
+
`REHASH`, `LUSERS` + `STATS`, `TRACE`, `WALLOPS`, `SETNAME`, plus
|
|
470
|
+
the formal drop of the S2S (`CONNECT`/`SQUIT`/`LINKS`) and obsolete
|
|
471
|
+
RFC 2812 (`SERVICE`/`SUMMON`/`USERS`) verbs. The protocol surface is
|
|
472
|
+
feature-complete for modern clients.
|
|
473
|
+
- **Phase 12 — IRCv3 extension sweep.** ✅ landed in **v0.6.0**: ten
|
|
474
|
+
negotiated caps (`account-notify`, `msgid`, `standard-replies`,
|
|
475
|
+
`MONITOR`, `labeled-response`, `sts`, `draft/typing`,
|
|
476
|
+
`draft/multiline`, `draft/read-marker`, `draft/pre-away`), the
|
|
477
|
+
ISUPPORT tokens for the new caps (`MONITOR`/`MULTILINE`/`TYPING`/
|
|
478
|
+
`STATUSMSG`/`EXTBAN`/`ACCOUNTEXTBAN`), the read-only user mode `S`
|
|
479
|
+
(TLS connected), and the async push fanout seam (Registry hooks →
|
|
480
|
+
`MONITOR` online/offline, `BroadcastWallops`-style caps-gated
|
|
481
|
+
delivery).
|
|
482
|
+
- **Phase 13 — IRC services.** ⬳ pending: `ServicesStore` port +
|
|
483
|
+
NickServ / ChanServ / HostServ / OperServ / MemoServ. Adapter
|
|
484
|
+
backends (D1 / DynamoDB) ship alongside; until then
|
|
485
|
+
`draft/read-marker` and `draft/pre-away` use in-memory reference
|
|
486
|
+
stores and do not survive an adapter restart.
|
|
487
|
+
- **Phase 14 — Web client.** ⬳ pending: vendored Kiwi IRC SPA served
|
|
488
|
+
from the Worker, WS Origin allowlist (CSWSH defense), landing page,
|
|
489
|
+
headless browser e2e.
|
|
490
|
+
- **Coverage hardening.** 🔄 partial as of **v0.6.0**: `aws-adapter`
|
|
491
|
+
and `aws-stack` now clear the 90% gate (CI boots DynamoDB Local via
|
|
492
|
+
a JRE; the CDK synth no longer re-bundles under coverage), and the
|
|
493
|
+
CF packages moved to istanbul to clear the v0.5.0 workerd-incompat.
|
|
494
|
+
Follow-ups drive each remaining sub-100% package to 100%.
|
|
351
495
|
|
|
352
496
|
---
|
|
353
497
|
|
|
@@ -356,21 +500,48 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
|
356
500
|
**Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
|
|
357
501
|
`PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
|
|
358
502
|
channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`,
|
|
359
|
-
`WHOWAS`, `MOTD`, `AWAY`, `OPER` (credential auth → `o` user mode),
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
503
|
+
`WHOWAS`, `MOTD`, `AWAY`, `OPER` (credential auth → `o` user mode), the
|
|
504
|
+
query verbs `VERSION`, `TIME`, `ADMIN`, `INFO`, `USERHOST`, `ISON`, the
|
|
505
|
+
modern-online-presence verb `MONITOR` (`+`/`-`/`C`/`L`/`S` with the
|
|
506
|
+
`730`/`731`/`732`/`734` numerics and async online/offline push), the
|
|
507
|
+
network-statistics verbs `LUSERS` and `STATS` (`u` uptime + `l` link-info
|
|
508
|
+
letters; unknown letters get the charybdis-style empty body), the
|
|
509
|
+
routing-diagnostic verb `TRACE` (oper-gated per-connection detail), the
|
|
510
|
+
realname-change verb `SETNAME`, and the oper verbs `KILL` (force-
|
|
511
|
+
disconnect), `REHASH` (config reload via `IrcRuntime.reloadConfig()`),
|
|
512
|
+
`WALLOPS` (global broadcast to every `+w` user).
|
|
513
|
+
|
|
514
|
+
(Out of scope: the S2S verbs `CONNECT`/`SQUIT`/`LINKS` and the obsolete
|
|
515
|
+
RFC 2812 verbs `SERVICE`/`SUMMON`/`USERS` are **formally dropped** as of
|
|
516
|
+
v0.5.0 — they return `421 ERR_UNKNOWNCOMMAND`, and their reserved
|
|
517
|
+
numerics have been removed. S2S linking is a PLAN non-goal.)
|
|
363
518
|
|
|
364
519
|
**Channel modes:** `o v b i k l t n m s p`.
|
|
365
|
-
**User modes:** `i`, `o` (local only), `w`, `s
|
|
520
|
+
**User modes:** `i`, `o` (local only), `w`, `s`, and the read-only `S`
|
|
521
|
+
(TLS connected — set by the transport at registration, surfaced in WHOIS
|
|
522
|
+
via `276 RPL_WHOISSECURE`, not settable via `MODE`).
|
|
366
523
|
|
|
367
524
|
**IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
|
|
368
|
-
`TAGMSG` command), `server-time`, `account-tag`, `
|
|
525
|
+
`TAGMSG` command), `server-time`, `account-tag`, `account-notify`
|
|
526
|
+
(pushes `ACCOUNT` on SASL login/logout), `echo-message`, `batch`,
|
|
369
527
|
`sasl` (`PLAIN` always; `EXTERNAL` via mTLS when a client-cert trust store
|
|
370
|
-
is bound),
|
|
371
|
-
`
|
|
372
|
-
`draft/chathistory
|
|
373
|
-
|
|
528
|
+
is bound), `multi-prefix`, `away-notify`, `chghost`, `invite-notify`,
|
|
529
|
+
`extended-join`, `msgid` (`@+msgid=<id>` on every PRIVMSG/NOTICE/TAGMSG,
|
|
530
|
+
shared between live and `draft/chathistory` replay), `standard-replies`
|
|
531
|
+
(`FAIL`/`WARN`/`NOTE` replacements for a curated numeric subset),
|
|
532
|
+
`MONITOR=<n>` (config-driven ceiling, default 30), `labeled-response`
|
|
533
|
+
(`+label=<id>` → `BATCH +id labeled-response … BATCH -id` wrapping),
|
|
534
|
+
`sts` (Strict Transport Security — conditionally advertised with
|
|
535
|
+
`duration`/`port`/optional `preload` from `ServerConfig` when configured,
|
|
536
|
+
so compliant clients upgrade from plaintext to TLS and pin the secure
|
|
537
|
+
listener), `draft/chathistory`, `safelist`, `draft/typing` (typing-indicator
|
|
538
|
+
broadcast), `draft/multiline=<n>` (multi-line `BATCH`, default 4096-byte
|
|
539
|
+
budget), `draft/read-marker` (per-account persisted last-read), and
|
|
540
|
+
`draft/pre-away` (per-account persisted away reason, replayed at
|
|
541
|
+
identify). Case-insensitive nick/channel comparison uses **RFC 1459**
|
|
542
|
+
case-mapping (advertised via `005 CASEMAPPING=rfc1459`). The new
|
|
543
|
+
`MONITOR` / `MULTILINE` / `TYPING` / `STATUSMSG` / `EXTBAN` /
|
|
544
|
+
`ACCOUNTEXTBAN` ISUPPORT tokens are advertised from `ServerConfig`.
|
|
374
545
|
|
|
375
546
|
**Transport:** deployed stacks (Cloudflare Workers, AWS API Gateway) speak
|
|
376
547
|
WebSocket text frames by default (one IRC message per frame, with tolerance
|
|
@@ -388,9 +559,17 @@ bridges a stock TCP client to a deployed WebSocket endpoint.
|
|
|
388
559
|
- `CHANGELOG.md` — per-release manifests (Keep a Changelog format), including
|
|
389
560
|
the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
|
|
390
561
|
gates), v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
|
|
391
|
-
ChannelRegistryDO, SASL account persistence),
|
|
562
|
+
ChannelRegistryDO, SASL account persistence), v0.4.0 (dual transport
|
|
392
563
|
`irc+tls :6697`, SASL `EXTERNAL`/mTLS, WHOWAS + remaining verbs, D1 account
|
|
393
|
-
store, Node 24/pnpm 11).
|
|
564
|
+
store, Node 24/pnpm 11), v0.5.0 (deferred IRC verbs `KILL` / `REHASH`
|
|
565
|
+
/ `LUSERS` + `STATS` / `TRACE` / `WALLOPS` / `SETNAME`, plus the formal
|
|
566
|
+
drop of the S2S and obsolete RFC 2812 verbs), and **v0.6.0** (the IRCv3
|
|
567
|
+
extension sweep — `account-notify`, `msgid`, `standard-replies`,
|
|
568
|
+
`MONITOR`, `labeled-response`, `sts`, `draft/typing`, `draft/multiline`,
|
|
569
|
+
`draft/read-marker`, `draft/pre-away`, plus user mode `S`; the
|
|
570
|
+
required-`serverName` gate; `tools/load-test`; aws-adapter / aws-stack
|
|
571
|
+
coverage unblocks; CF packages moved to istanbul; deploys now
|
|
572
|
+
manual-only).
|
|
394
573
|
- `docs/Cloudflare-TCP-Deployment.md` and `docs/AWS-TCP-Deployment.md` —
|
|
395
574
|
end-to-end guides for the `:6697` TCP+TLS variants (Spectrum/Container on
|
|
396
575
|
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
|
|
@@ -60,6 +60,39 @@ export AWS_DEFAULT_REGION=us-east-1
|
|
|
60
60
|
LOCALSTACK=1 pnpm test
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Test-mode bundle stub (why coverage does not re-bundle)
|
|
64
|
+
|
|
65
|
+
The stack ships four `NodejsFunction` constructs (the wss `IrcHandler`,
|
|
66
|
+
`IrcSweeper`, `IrcPingChecker`, and the optional NLB `IrcNlbHandler`).
|
|
67
|
+
Each one invokes esbuild at synth time to bundle the shared handler
|
|
68
|
+
entry. Repeated across the ~80 synth assertions in `tests/stack.test.ts`,
|
|
69
|
+
that meant the bundler ran **dozens to hundreds of times per coverage
|
|
70
|
+
invocation**, exhausting CI memory before vitest could print its
|
|
71
|
+
coverage table — the failure mode presented as a wall of
|
|
72
|
+
`Bundling asset TestStack/IrcHandler/Code/Stage…` lines followed by
|
|
73
|
+
`[ELIFECYCLE] Command failed with exit code N` and no report.
|
|
74
|
+
|
|
75
|
+
The fix is a single env-var toggle consumed by `makeLambda()` in
|
|
76
|
+
`src/aws-stack.ts`:
|
|
77
|
+
|
|
78
|
+
| Env var | Effect on the synth |
|
|
79
|
+
|-----------------------------------|-----------------------------------------------------------------------|
|
|
80
|
+
| `IRC_AWS_STACK_TEST_NO_BUNDLE=1` | Every Lambda is emitted as an inline `Code.fromInline('void 0')` zip; esbuild is not invoked at all. CloudFormation shape (runtime, env vars, IAM grants, route wiring, …) is identical to the production synth. |
|
|
81
|
+
| _(unset)_ | Production path: every Lambda is a real `NodejsFunction` whose asset is esbuild-bundled at synth time. This is what `cdk synth` / `cdk deploy` run. |
|
|
82
|
+
|
|
83
|
+
`tests/vitest.setup.ts` sets the flag once per test file so the entire
|
|
84
|
+
`pnpm test` / `pnpm coverage` run stays on the stub path. Production
|
|
85
|
+
synth (`bin/aws.ts` via the CDK CLI) never loads vitest, so it cannot
|
|
86
|
+
accidentally inherit the flag.
|
|
87
|
+
|
|
88
|
+
A small `tests/synth-no-bundle.test.ts` locks both paths in place: it
|
|
89
|
+
asserts the stub emits `Code: { ZipFile }` when the flag is set, and
|
|
90
|
+
exercises the production `NodejsFunction` return once (with the flag
|
|
91
|
+
explicitly unset) so the production branch stays covered. The
|
|
92
|
+
end-to-end guard lives in `tests/coverage-command.test.ts`, which
|
|
93
|
+
spawns `pnpm --filter @serverless-ircd/aws-stack coverage` in a
|
|
94
|
+
subprocess and asserts the report prints an `All files` row.
|
|
95
|
+
|
|
63
96
|
## Stack outputs
|
|
64
97
|
|
|
65
98
|
| Output | Description |
|
|
@@ -70,4 +103,5 @@ LOCALSTACK=1 pnpm test
|
|
|
70
103
|
## Handoff
|
|
71
104
|
|
|
72
105
|
Lambda runtime logic, DynamoDB access patterns, and the `AwsRuntime`
|
|
73
|
-
implementation
|
|
106
|
+
implementation land with the AWS runtime work. The scheduler/sweeper is
|
|
107
|
+
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';
|
|
@@ -32,8 +37,8 @@ import {
|
|
|
32
37
|
import { LambdaTarget } from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
|
|
33
38
|
import { Rule, Schedule } from 'aws-cdk-lib/aws-events';
|
|
34
39
|
import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
|
|
35
|
-
import { Runtime } from 'aws-cdk-lib/aws-lambda';
|
|
36
|
-
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
40
|
+
import { Code, Function as Lambda, Runtime } from 'aws-cdk-lib/aws-lambda';
|
|
41
|
+
import { NodejsFunction, type NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
37
42
|
import type { Construct } from 'constructs';
|
|
38
43
|
|
|
39
44
|
/** Stage name served by the auto-deploying WebSocket stage. */
|
|
@@ -59,8 +64,14 @@ const PING_CHECKER_SCHEDULE_RATE = Duration.minutes(1);
|
|
|
59
64
|
*/
|
|
60
65
|
const DEFAULT_ENVIRONMENT_NAME = 'staging';
|
|
61
66
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Fallback server identity, used when the corresponding prop is omitted.
|
|
69
|
+
* Deliberately not `irc.example.com` (the retired placeholder) so a
|
|
70
|
+
* deployment that forgets to override advertises an obviously-local name
|
|
71
|
+
* instead of a deceptive example domain. Production stacks MUST pass
|
|
72
|
+
* `serverName` explicitly.
|
|
73
|
+
*/
|
|
74
|
+
const DEFAULT_SERVER_NAME = 'irc.localhost';
|
|
64
75
|
const DEFAULT_NETWORK_NAME = 'ExampleNet';
|
|
65
76
|
const DEFAULT_MOTD_LINES = [
|
|
66
77
|
'Welcome to the ServerlessIRCd deployment.',
|
|
@@ -69,6 +80,22 @@ const DEFAULT_MOTD_LINES = [
|
|
|
69
80
|
|
|
70
81
|
const HERE = fileURLToPath(new URL('.', import.meta.url));
|
|
71
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Env-var flag that flips {@link makeLambda} onto a stub code path which
|
|
85
|
+
* emits an inline Lambda zip and skips esbuild entirely. The IrcAwsStack
|
|
86
|
+
* test suite synthesizes the stack dozens of times per coverage run; if
|
|
87
|
+
* each synth invoked esbuild the runner exhausted its memory budget
|
|
88
|
+
* before the vitest coverage table could print. The stub preserves every
|
|
89
|
+
* CloudFormation shape the rest of the test suite asserts on (runtime,
|
|
90
|
+
* env vars, IAM grants, route wiring, …) while making the bundling step
|
|
91
|
+
* a no-op.
|
|
92
|
+
*
|
|
93
|
+
* Intentionally a plain `process.env` read (not a CDK context var or
|
|
94
|
+
* construct prop) so the toggle lives in one place — `tests/vitest.setup.ts`
|
|
95
|
+
* — and cannot leak into a production synth driven by `bin/aws.ts`.
|
|
96
|
+
*/
|
|
97
|
+
const TEST_NO_BUNDLE_FLAG = 'IRC_AWS_STACK_TEST_NO_BUNDLE';
|
|
98
|
+
|
|
72
99
|
/**
|
|
73
100
|
* Construct props for {@link IrcAwsStack}. Acts as the single
|
|
74
101
|
* configuration bag extended by deploy-time knobs (environment name,
|
|
@@ -88,7 +115,8 @@ export interface IrcStackProps extends StackProps {
|
|
|
88
115
|
|
|
89
116
|
/**
|
|
90
117
|
* Server name advertised to clients (`SERVER_NAME` / the `001`
|
|
91
|
-
* welcome source).
|
|
118
|
+
* welcome source). Falls back to `irc.localhost` when omitted; production
|
|
119
|
+
* stacks MUST pass an explicit value.
|
|
92
120
|
*/
|
|
93
121
|
readonly serverName?: string;
|
|
94
122
|
|
|
@@ -145,7 +173,7 @@ export class IrcAwsStack extends Stack {
|
|
|
145
173
|
}),
|
|
146
174
|
);
|
|
147
175
|
|
|
148
|
-
const handler =
|
|
176
|
+
const handler = makeLambda(this, 'IrcHandler', {
|
|
149
177
|
runtime: Runtime.NODEJS_24_X,
|
|
150
178
|
entry: handlerEntry(),
|
|
151
179
|
handler: 'handler',
|
|
@@ -161,7 +189,7 @@ export class IrcAwsStack extends Stack {
|
|
|
161
189
|
// bundle (tree-shaken to the sweeper path by esbuild at synth time).
|
|
162
190
|
// Catches connections that vanished without APIGW emitting $disconnect;
|
|
163
191
|
// see packages/aws-adapter/src/handlers/sweeper.ts.
|
|
164
|
-
const sweeper =
|
|
192
|
+
const sweeper = makeLambda(this, 'IrcSweeper', {
|
|
165
193
|
runtime: Runtime.NODEJS_24_X,
|
|
166
194
|
entry: handlerEntry(),
|
|
167
195
|
handler: 'sweeperHandler',
|
|
@@ -175,7 +203,7 @@ export class IrcAwsStack extends Stack {
|
|
|
175
203
|
// those that never answer; see
|
|
176
204
|
// packages/aws-adapter/src/handlers/ping-checker.ts. The Cloudflare
|
|
177
205
|
// adapter implements the equivalent flow inside `ConnectionDO.alarm()`.
|
|
178
|
-
const pingChecker =
|
|
206
|
+
const pingChecker = makeLambda(this, 'IrcPingChecker', {
|
|
179
207
|
runtime: Runtime.NODEJS_24_X,
|
|
180
208
|
entry: handlerEntry(),
|
|
181
209
|
handler: 'pingCheckerHandler',
|
|
@@ -191,9 +219,6 @@ export class IrcAwsStack extends Stack {
|
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
const webSocketApi = new WebSocketApi(this, 'IrcWsApi', {
|
|
194
|
-
connectRouteOptions: {
|
|
195
|
-
integration: new WebSocketLambdaIntegration('ConnectIntegration', handler),
|
|
196
|
-
},
|
|
197
222
|
disconnectRouteOptions: {
|
|
198
223
|
integration: new WebSocketLambdaIntegration('DisconnectIntegration', handler),
|
|
199
224
|
},
|
|
@@ -202,6 +227,32 @@ export class IrcAwsStack extends Stack {
|
|
|
202
227
|
},
|
|
203
228
|
});
|
|
204
229
|
|
|
230
|
+
// The `$connect` route is added explicitly (rather than via
|
|
231
|
+
// `connectRouteOptions`) so the negotiated IRCv3 WebSocket subprotocol can
|
|
232
|
+
// be echoed back to the client. API Gateway selects the WebSocket
|
|
233
|
+
// subprotocol from the `$connect` integration response, so the route is
|
|
234
|
+
// configured to return a response and an `IntegrationResponse` maps the
|
|
235
|
+
// `Sec-WebSocket-Protocol` header from the Lambda's reply onto the
|
|
236
|
+
// handshake. The Lambda reads the client's offer, selects the first
|
|
237
|
+
// supported entry, and returns it in its response header (see
|
|
238
|
+
// `handleConnect` / `dispatch`).
|
|
239
|
+
const connectRoute = webSocketApi.addRoute('$connect', {
|
|
240
|
+
integration: new WebSocketLambdaIntegration('ConnectIntegration', handler),
|
|
241
|
+
returnResponse: true,
|
|
242
|
+
});
|
|
243
|
+
const connectIntegration = connectRoute.node.findChild(
|
|
244
|
+
'ConnectIntegration',
|
|
245
|
+
) as WebSocketIntegration;
|
|
246
|
+
new CfnIntegrationResponse(this, 'ConnectSubprotocolResponse', {
|
|
247
|
+
apiId: webSocketApi.apiId,
|
|
248
|
+
integrationId: connectIntegration.integrationId,
|
|
249
|
+
integrationResponseKey: '$default',
|
|
250
|
+
responseParameters: {
|
|
251
|
+
'method.response.header.Sec-WebSocket-Protocol':
|
|
252
|
+
'integration.response.header.Sec-WebSocket-Protocol',
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
205
256
|
// The stage name is intentionally NOT suffixed with the environment:
|
|
206
257
|
// each environment is its own CloudFormation stack (and therefore its
|
|
207
258
|
// own WebSocket API), so `prod` in every env resolves to a distinct
|
|
@@ -290,7 +341,7 @@ export class IrcAwsStack extends Stack {
|
|
|
290
341
|
// Shares the bundled aws-adapter code (tree-shaken to the nlb-stream
|
|
291
342
|
// path by esbuild at synth time). Reuses the same DynamoDB tables,
|
|
292
343
|
// server identity, and MANAGEMENT_URL as the wss handler.
|
|
293
|
-
const nlbHandler =
|
|
344
|
+
const nlbHandler = makeLambda(this, 'IrcNlbHandler', {
|
|
294
345
|
runtime: Runtime.NODEJS_24_X,
|
|
295
346
|
entry: handlerEntry(),
|
|
296
347
|
handler: 'nlbStreamHandler',
|
|
@@ -363,6 +414,48 @@ function handlerEntry(): string {
|
|
|
363
414
|
return resolve(HERE, '..', '..', '..', 'packages', 'aws-adapter', 'src', 'handlers', 'index.ts');
|
|
364
415
|
}
|
|
365
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Internal Lambda-construct props: `NodejsFunctionProps` narrowed so the
|
|
419
|
+
* runtime and handler are required. Every call site in the stack already
|
|
420
|
+
* passes both explicitly; this type just makes that invariant visible to
|
|
421
|
+
* the compiler so {@link makeLambda} can hand them straight through to
|
|
422
|
+
* the stub `Function` constructor without a non-null assertion.
|
|
423
|
+
*/
|
|
424
|
+
type StrictLambdaProps = NodejsFunctionProps & {
|
|
425
|
+
readonly runtime: Runtime;
|
|
426
|
+
readonly handler: string;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Builds a Lambda function for the IRC stack, selecting between the real
|
|
431
|
+
* esbuild-bundled `NodejsFunction` and a no-op stub `Function` based on
|
|
432
|
+
* {@link TEST_NO_BUNDLE_FLAG}.
|
|
433
|
+
*
|
|
434
|
+
* Production synth (`bin/aws.ts`) never sets the flag, so it always lands
|
|
435
|
+
* on the `NodejsFunction` branch and produces a real bundled asset. The
|
|
436
|
+
* test suite sets the flag via `tests/vitest.setup.ts`, which routes
|
|
437
|
+
* every Lambda onto the inline-zip stub so the synth does not invoke
|
|
438
|
+
* esbuild and the coverage pipeline can complete.
|
|
439
|
+
*
|
|
440
|
+
* The stub mirrors only the fields the rest of the stack mutates
|
|
441
|
+
* post-construction (`runtime`, `handler`, env vars, IAM grants) — it
|
|
442
|
+
* deliberately ignores `entry` and `bundling` because both are
|
|
443
|
+
* meaningless once esbuild is bypassed.
|
|
444
|
+
*
|
|
445
|
+
* Returns the base `Function` type (not `NodejsFunction`) so callers
|
|
446
|
+
* cannot reach for bundler-only APIs on the stub path.
|
|
447
|
+
*/
|
|
448
|
+
function makeLambda(scope: Construct, id: string, props: StrictLambdaProps): Lambda {
|
|
449
|
+
if (process.env[TEST_NO_BUNDLE_FLAG] === '1') {
|
|
450
|
+
return new Lambda(scope, id, {
|
|
451
|
+
runtime: props.runtime,
|
|
452
|
+
handler: props.handler,
|
|
453
|
+
code: Code.fromInline('void 0'),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return new NodejsFunction(scope, id, props);
|
|
457
|
+
}
|
|
458
|
+
|
|
366
459
|
/**
|
|
367
460
|
* Builds the physical DynamoDB table name for a logical id under the
|
|
368
461
|
* given environment. Capitalises the environment's first letter so it
|