serverless-ircd 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +349 -18
- package/README.md +132 -30
- package/apps/aws-stack/README.md +6 -5
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +150 -10
- package/apps/aws-stack/tests/stack.test.ts +145 -4
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +149 -24
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +75 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +96 -9
- package/packages/aws-adapter/src/handlers/default.ts +98 -7
- package/packages/aws-adapter/src/handlers/index.ts +109 -4
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +8 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +174 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +302 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +5 -1
- package/packages/cf-adapter/src/cf-runtime.ts +68 -5
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +278 -85
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +54 -11
- package/packages/cf-adapter/src/index.ts +11 -8
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +12 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
- package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +15 -8
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/wrangler.test.toml +15 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +24 -3
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +17 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +6 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +4 -8
- package/packages/irc-core/src/commands/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +65 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +33 -30
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +360 -12
- package/packages/irc-core/src/protocol/numerics.ts +48 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +46 -2
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +57 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +102 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +393 -16
- package/packages/irc-server/src/dispatch.ts +1 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +15 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +104 -0
- package/packages/irc-server/tests/actor.test.ts +1489 -4
- package/packages/irc-server/tests/dispatch.test.ts +37 -17
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +5 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +141 -3
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +10 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +9 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +107 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -443
package/README.md
CHANGED
|
@@ -4,22 +4,26 @@ A serverless IRC daemon where the IRC protocol logic lives in a pure,
|
|
|
4
4
|
platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
|
|
5
5
|
(Durable Objects) or **AWS** (API Gateway WebSockets + Lambda + DynamoDB).
|
|
6
6
|
|
|
7
|
-
One TypeScript codebase. Two serverless substrates.
|
|
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.
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
>
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
-
>
|
|
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
|
+
> clients — Cloudflare via Spectrum + a Container origin, AWS via a
|
|
16
|
+
> Network Load Balancer + Lambda streaming — alongside the WebSocket
|
|
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.
|
|
23
27
|
|
|
24
28
|
---
|
|
25
29
|
|
|
@@ -70,6 +74,16 @@ Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
|
|
|
70
74
|
`packages/in-memory-runtime` is the reference implementation of
|
|
71
75
|
`IrcRuntime`, used by integration tests and the local CLI server.
|
|
72
76
|
|
|
77
|
+
**Two transports, one core.** Both adapters accept WebSocket text frames
|
|
78
|
+
(the serverless default) **and** a raw `irc+tls :6697` (TLS-over-TCP)
|
|
79
|
+
surface for stock IRC clients. The TCP+TLS edge is platform-specific —
|
|
80
|
+
Cloudflare **Spectrum** terminates TLS and forwards plaintext TCP to a
|
|
81
|
+
stateful **Container** origin (`apps/cf-tcp-container`); AWS terminates
|
|
82
|
+
TLS at a **Network Load Balancer** and invokes a **Lambda streaming**
|
|
83
|
+
function. Both feed the same `ConnectionActor` through a `Transport`
|
|
84
|
+
seam (`WsTextFrameTransport` vs. `TcpByteStreamTransport`); the
|
|
85
|
+
parser/reducer/dispatch pipeline is identical.
|
|
86
|
+
|
|
73
87
|
### The key idea: pure reducers + location-of-authority
|
|
74
88
|
|
|
75
89
|
Each command handler is a pure function:
|
|
@@ -111,12 +125,14 @@ ServerlessIRCd/
|
|
|
111
125
|
│ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime
|
|
112
126
|
├── apps/
|
|
113
127
|
│ ├── cf-worker/ worker entry, DO migrations, bindings
|
|
114
|
-
│ ├──
|
|
128
|
+
│ ├── cf-tcp-container/ Spectrum + Container origin for irc+tls :6697
|
|
129
|
+
│ ├── aws-stack/ CDK stack (APIGW WS + NLB + Lambda streaming + DynamoDB)
|
|
115
130
|
│ └── local-cli/ runnable WS + TCP server using in-memory-runtime
|
|
116
131
|
├── tools/
|
|
117
132
|
│ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
|
|
118
133
|
│ ├── ci-hardening/ coverage-gate + mutation-config validators
|
|
119
|
-
│
|
|
134
|
+
│ ├── seed-aws-accounts.ts scrypt-hash SASL PLAIN accounts into DynamoDB
|
|
135
|
+
│ └── seed-cf-accounts.ts scrypt-hash SASL PLAIN accounts into Cloudflare D1
|
|
120
136
|
├── pnpm-workspace.yaml turbo.json tsconfig.base.json
|
|
121
137
|
└── README.md CHANGELOG.md
|
|
122
138
|
```
|
|
@@ -127,7 +143,7 @@ ServerlessIRCd/
|
|
|
127
143
|
|
|
128
144
|
| Concern | Choice |
|
|
129
145
|
|------------------|---------------------------------------------------|
|
|
130
|
-
| Runtime | Node ≥
|
|
146
|
+
| Runtime | Node ≥ 24, ES2022+ |
|
|
131
147
|
| Package mgr | pnpm workspaces |
|
|
132
148
|
| Build cache | turbo |
|
|
133
149
|
| Language | TypeScript (strict, `exactOptionalPropertyTypes`) |
|
|
@@ -141,9 +157,12 @@ ServerlessIRCd/
|
|
|
141
157
|
|
|
142
158
|
## Getting started
|
|
143
159
|
|
|
144
|
-
Requires Node ≥
|
|
160
|
+
Requires Node ≥ 24 and pnpm 11. Enable corepack so the `packageManager`
|
|
161
|
+
pin is resolved automatically:
|
|
145
162
|
|
|
146
163
|
```bash
|
|
164
|
+
corepack enable # lets the pinned pnpm@11 run on any Node ≥ 24
|
|
165
|
+
|
|
147
166
|
pnpm install # install workspace deps
|
|
148
167
|
|
|
149
168
|
pnpm build # build all packages (turbo)
|
|
@@ -283,6 +302,42 @@ against any line-oriented WebSocket endpoint.
|
|
|
283
302
|
|
|
284
303
|
---
|
|
285
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
|
+
|
|
286
341
|
## Testing strategy
|
|
287
342
|
|
|
288
343
|
This project follows strict TDD (Red → Green → Refactor) — every reducer is
|
|
@@ -322,10 +377,35 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
|
322
377
|
- **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
|
|
323
378
|
✅ landed in **v0.2.0** (staging auto-deploy via CI; prod deploy is manual).
|
|
324
379
|
- **Phase 5** — Observability, security hardening, config, CI gates.
|
|
325
|
-
✅ landed (logger port, server-password, cloaking,
|
|
326
|
-
coverage + mutation gates, OPER credential auth,
|
|
327
|
-
config loader
|
|
328
|
-
- **Phase 6** —
|
|
380
|
+
✅ landed across v0.2.0–v0.4.0 (logger port, server-password, cloaking,
|
|
381
|
+
admission limits, coverage + mutation gates, OPER credential auth, the
|
|
382
|
+
Cloudflare config loader, and AWS max-clients admission).
|
|
383
|
+
- **Phase 6** — TLS hardening & raw TCP transport (`irc+tls :6697`).
|
|
384
|
+
✅ landed in **v0.4.0**: generalized `ConnectionActor` transport seam,
|
|
385
|
+
Cloudflare Spectrum + Container origin, AWS NLB + Lambda streaming,
|
|
386
|
+
mTLS → SASL `EXTERNAL`, transport-parametrized contract suite, ADR-009.
|
|
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.
|
|
329
409
|
|
|
330
410
|
---
|
|
331
411
|
|
|
@@ -333,25 +413,40 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
|
333
413
|
|
|
334
414
|
**Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
|
|
335
415
|
`PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
|
|
336
|
-
channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`,
|
|
337
|
-
`AWAY`, `OPER` (credential auth → `o` user mode)
|
|
416
|
+
channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`,
|
|
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.)
|
|
338
430
|
|
|
339
431
|
**Channel modes:** `o v b i k l t n m s p`.
|
|
340
432
|
**User modes:** `i`, `o` (local only), `w`, `s`.
|
|
341
433
|
|
|
342
434
|
**IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
|
|
343
435
|
`TAGMSG` command), `server-time`, `account-tag`, `echo-message`, `batch`,
|
|
344
|
-
`sasl` (`PLAIN`
|
|
436
|
+
`sasl` (`PLAIN` always; `EXTERNAL` via mTLS when a client-cert trust store
|
|
437
|
+
is bound),
|
|
345
438
|
`multi-prefix`, `away-notify`, `chghost`, `invite-notify`, `extended-join`,
|
|
346
439
|
`draft/chathistory`, `safelist`. Case-insensitive nick/channel comparison
|
|
347
440
|
uses **RFC 1459** case-mapping (advertised via `005 CASEMAPPING=rfc1459`).
|
|
348
441
|
|
|
349
442
|
**Transport:** deployed stacks (Cloudflare Workers, AWS API Gateway) speak
|
|
350
|
-
WebSocket text frames
|
|
351
|
-
`\r\n`-joined frames)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
443
|
+
WebSocket text frames by default (one IRC message per frame, with tolerance
|
|
444
|
+
for `\r\n`-joined frames), and optionally expose a raw `irc+tls :6697`
|
|
445
|
+
(TLS-over-TCP) surface — Cloudflare via **Spectrum + Container**, AWS via a
|
|
446
|
+
**Network Load Balancer + Lambda streaming** function. The local CLI
|
|
447
|
+
additionally binds a plain RFC TCP listener for direct IRC-client access,
|
|
448
|
+
and the [`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder)
|
|
449
|
+
bridges a stock TCP client to a deployed WebSocket endpoint.
|
|
355
450
|
|
|
356
451
|
---
|
|
357
452
|
|
|
@@ -359,8 +454,15 @@ WebSocket endpoint through the local
|
|
|
359
454
|
|
|
360
455
|
- `CHANGELOG.md` — per-release manifests (Keep a Changelog format), including
|
|
361
456
|
the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
|
|
362
|
-
gates)
|
|
363
|
-
ChannelRegistryDO, SASL account persistence).
|
|
457
|
+
gates), v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
|
|
458
|
+
ChannelRegistryDO, SASL account persistence), v0.4.0 (dual transport
|
|
459
|
+
`irc+tls :6697`, SASL `EXTERNAL`/mTLS, WHOWAS + remaining verbs, D1 account
|
|
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).
|
|
463
|
+
- `docs/Cloudflare-TCP-Deployment.md` and `docs/AWS-TCP-Deployment.md` —
|
|
464
|
+
end-to-end guides for the `:6697` TCP+TLS variants (Spectrum/Container on
|
|
465
|
+
CF, NLB + Lambda streaming on AWS), including mTLS trust-store setup.
|
|
364
466
|
- `README.md` in each `packages/*` and `apps/*` — per-package notes (e.g.
|
|
365
467
|
`packages/aws-adapter/README.md` for the DynamoDB-Local test setup,
|
|
366
468
|
`apps/aws-stack/README.md` for CDK commands and localstack validation).
|
package/apps/aws-stack/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# `@serverless-ircd/aws-stack`
|
|
2
2
|
|
|
3
3
|
AWS CDK v2 stack that provisions the AWS substrate for ServerlessIRCd
|
|
4
|
-
(PLAN §6.2): an API Gateway v2 WebSocket API, a single Node
|
|
4
|
+
(PLAN §6.2): an API Gateway v2 WebSocket API, a single Node 24 Lambda wired
|
|
5
5
|
to the `$connect` / `$disconnect` / `$default` routes, five DynamoDB tables
|
|
6
6
|
(`Connections`, `ChannelMeta`, `ChannelMembers`, `Nicks`, `Accounts`), and the
|
|
7
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
|
|
|
15
|
-
- Node.js
|
|
15
|
+
- Node.js 24+
|
|
16
16
|
- pnpm (workspace root installs this package)
|
|
17
17
|
- An AWS account with credentials configured
|
|
18
18
|
(`aws configure`, `AWS_PROFILE`, or `SSO`)
|
|
@@ -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.
|
|
@@ -38,11 +38,18 @@ const motdLines = Array.isArray(rawMotd)
|
|
|
38
38
|
? rawMotd.split('\n')
|
|
39
39
|
: undefined;
|
|
40
40
|
|
|
41
|
+
// TCP+TLS (irc+tls :6697) is opt-in: only provisioned when this context
|
|
42
|
+
// var is supplied. When omitted, only the wss (API Gateway WebSocket)
|
|
43
|
+
// path deploys (the default documented in docs/AWS-Deployment.md). See
|
|
44
|
+
// docs/AWS-TCP-Deployment.md.
|
|
45
|
+
const tcpTlsDomainName = app.node.tryGetContext('tcpTlsDomainName') as string | undefined;
|
|
46
|
+
|
|
41
47
|
const stackProps: IrcStackProps = {
|
|
42
48
|
environmentName,
|
|
43
49
|
...(serverName !== undefined ? { serverName } : {}),
|
|
44
50
|
...(networkName !== undefined ? { networkName } : {}),
|
|
45
51
|
...(motdLines !== undefined ? { motdLines } : {}),
|
|
52
|
+
...(tcpTlsDomainName !== undefined ? { tcpTlsDomainName } : {}),
|
|
46
53
|
...(account && region ? { env: { account, region } } : {}),
|
|
47
54
|
};
|
|
48
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/aws-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "AWS CDK v2 stack: API Gateway v2 WebSocket API, Lambda, DynamoDB tables, least-privilege IAM",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"smoke:staging": "node scripts/smoke.mjs"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"aws-cdk": "^2.
|
|
23
|
-
"aws-cdk-lib": "^2.
|
|
22
|
+
"aws-cdk": "^2.262.0",
|
|
23
|
+
"aws-cdk-lib": "^2.262.0",
|
|
24
24
|
"constructs": "^10.4.0",
|
|
25
25
|
"@types/node": "^26.1.1",
|
|
26
26
|
"@types/ws": "^8.5.13",
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"@serverless-ircd/aws-adapter": "workspace:*"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
39
|
+
"node": ">=24"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* • 5 DynamoDB tables sourced from `@serverless-ircd/aws-adapter/cdk-table-defs`
|
|
5
5
|
* (Connections, ChannelMeta, ChannelMembers, Nicks, Accounts).
|
|
6
|
-
* • ONE Node
|
|
6
|
+
* • ONE Node 24 `NodejsFunction` bundling the AWS Lambda handler
|
|
7
7
|
* (`$connect` / `$disconnect` / `$default` dispatched on
|
|
8
8
|
* `event.requestContext.routeKey`).
|
|
9
|
-
* • ONE Node
|
|
9
|
+
* • ONE Node 24 `NodejsFunction` for the gone-connection sweeper
|
|
10
10
|
* (`sweeperHandler`), invoked on a fixed EventBridge schedule.
|
|
11
11
|
* • API Gateway v2 WebSocket API + auto-deploying `prod` stage.
|
|
12
12
|
* • Least-privilege IAM: scoped DynamoDB data-plane grants per table +
|
|
@@ -17,9 +17,24 @@ 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';
|
|
27
|
+
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
22
28
|
import { Table } from 'aws-cdk-lib/aws-dynamodb';
|
|
29
|
+
import { Vpc } from 'aws-cdk-lib/aws-ec2';
|
|
30
|
+
import {
|
|
31
|
+
NetworkListener,
|
|
32
|
+
NetworkLoadBalancer,
|
|
33
|
+
NetworkTargetGroup,
|
|
34
|
+
Protocol,
|
|
35
|
+
TargetType,
|
|
36
|
+
} from 'aws-cdk-lib/aws-elasticloadbalancingv2';
|
|
37
|
+
import { LambdaTarget } from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
|
|
23
38
|
import { Rule, Schedule } from 'aws-cdk-lib/aws-events';
|
|
24
39
|
import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
|
|
25
40
|
import { Runtime } from 'aws-cdk-lib/aws-lambda';
|
|
@@ -29,6 +44,9 @@ import type { Construct } from 'constructs';
|
|
|
29
44
|
/** Stage name served by the auto-deploying WebSocket stage. */
|
|
30
45
|
const STAGE_NAME = 'prod';
|
|
31
46
|
|
|
47
|
+
/** IRC over TLS port served by the NLB listener (RFC-compliant :6697). */
|
|
48
|
+
const IRC_TLS_PORT = 6697;
|
|
49
|
+
|
|
32
50
|
/** Fixed rate for the gone-connection sweeper (matches the architecture doc). */
|
|
33
51
|
const SWEEPER_SCHEDULE_RATE = Duration.minutes(5);
|
|
34
52
|
|
|
@@ -91,6 +109,21 @@ export interface IrcStackProps extends StackProps {
|
|
|
91
109
|
* a `string[]` correctly. Defaults to a short two-line banner.
|
|
92
110
|
*/
|
|
93
111
|
readonly motdLines?: string[];
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Domain name for the TCP+TLS (IRC `:6697`) endpoint.
|
|
115
|
+
*
|
|
116
|
+
* When provided, the stack provisions:
|
|
117
|
+
* - An ACM certificate for this domain.
|
|
118
|
+
* - A Network Load Balancer with a `TLS` listener on `:6697`.
|
|
119
|
+
* - A Lambda streaming target (the `nlbStreamHandler` export).
|
|
120
|
+
*
|
|
121
|
+
* The NLB terminates TLS at the edge and forwards plaintext TCP
|
|
122
|
+
* invocations to the Lambda. Connection identity is derived from
|
|
123
|
+
* NLB flow metadata (source IP + port). When omitted, only the wss
|
|
124
|
+
* (WebSocket) transport is deployed.
|
|
125
|
+
*/
|
|
126
|
+
readonly tcpTlsDomainName?: string;
|
|
94
127
|
}
|
|
95
128
|
|
|
96
129
|
export class IrcAwsStack extends Stack {
|
|
@@ -118,11 +151,11 @@ export class IrcAwsStack extends Stack {
|
|
|
118
151
|
);
|
|
119
152
|
|
|
120
153
|
const handler = new NodejsFunction(this, 'IrcHandler', {
|
|
121
|
-
runtime: Runtime.
|
|
154
|
+
runtime: Runtime.NODEJS_24_X,
|
|
122
155
|
entry: handlerEntry(),
|
|
123
156
|
handler: 'handler',
|
|
124
157
|
bundling: {
|
|
125
|
-
// AWS SDK v3 is in the Lambda Node
|
|
158
|
+
// AWS SDK v3 is in the Lambda Node 24 runtime; exclude it so the
|
|
126
159
|
// bundle stays small. irc-core / irc-server are bundled because
|
|
127
160
|
// they are first-party.
|
|
128
161
|
externalModules: ['@aws-sdk/*'],
|
|
@@ -134,7 +167,7 @@ export class IrcAwsStack extends Stack {
|
|
|
134
167
|
// Catches connections that vanished without APIGW emitting $disconnect;
|
|
135
168
|
// see packages/aws-adapter/src/handlers/sweeper.ts.
|
|
136
169
|
const sweeper = new NodejsFunction(this, 'IrcSweeper', {
|
|
137
|
-
runtime: Runtime.
|
|
170
|
+
runtime: Runtime.NODEJS_24_X,
|
|
138
171
|
entry: handlerEntry(),
|
|
139
172
|
handler: 'sweeperHandler',
|
|
140
173
|
bundling: {
|
|
@@ -148,7 +181,7 @@ export class IrcAwsStack extends Stack {
|
|
|
148
181
|
// packages/aws-adapter/src/handlers/ping-checker.ts. The Cloudflare
|
|
149
182
|
// adapter implements the equivalent flow inside `ConnectionDO.alarm()`.
|
|
150
183
|
const pingChecker = new NodejsFunction(this, 'IrcPingChecker', {
|
|
151
|
-
runtime: Runtime.
|
|
184
|
+
runtime: Runtime.NODEJS_24_X,
|
|
152
185
|
entry: handlerEntry(),
|
|
153
186
|
handler: 'pingCheckerHandler',
|
|
154
187
|
bundling: {
|
|
@@ -163,9 +196,6 @@ export class IrcAwsStack extends Stack {
|
|
|
163
196
|
}
|
|
164
197
|
|
|
165
198
|
const webSocketApi = new WebSocketApi(this, 'IrcWsApi', {
|
|
166
|
-
connectRouteOptions: {
|
|
167
|
-
integration: new WebSocketLambdaIntegration('ConnectIntegration', handler),
|
|
168
|
-
},
|
|
169
199
|
disconnectRouteOptions: {
|
|
170
200
|
integration: new WebSocketLambdaIntegration('DisconnectIntegration', handler),
|
|
171
201
|
},
|
|
@@ -174,6 +204,32 @@ export class IrcAwsStack extends Stack {
|
|
|
174
204
|
},
|
|
175
205
|
});
|
|
176
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
|
+
|
|
177
233
|
// The stage name is intentionally NOT suffixed with the environment:
|
|
178
234
|
// each environment is its own CloudFormation stack (and therefore its
|
|
179
235
|
// own WebSocket API), so `prod` in every env resolves to a distinct
|
|
@@ -232,6 +288,90 @@ export class IrcAwsStack extends Stack {
|
|
|
232
288
|
targets: [new LambdaFunction(pingChecker)],
|
|
233
289
|
});
|
|
234
290
|
|
|
291
|
+
// -------------------------------------------------------------------------
|
|
292
|
+
// TCP+TLS adapter via Network Load Balancer.
|
|
293
|
+
//
|
|
294
|
+
// An NLB with a `TLS` listener terminates TLS at the edge and invokes a
|
|
295
|
+
// Lambda streaming function (`nlbStreamHandler`) for each chunk of client
|
|
296
|
+
// data. The Lambda response body is streamed back over the held-open TCP
|
|
297
|
+
// connection. Only provisioned when `tcpTlsDomainName` is supplied.
|
|
298
|
+
// -------------------------------------------------------------------------
|
|
299
|
+
if (props.tcpTlsDomainName !== undefined) {
|
|
300
|
+
const tcpTlsDomain = props.tcpTlsDomainName;
|
|
301
|
+
|
|
302
|
+
// ACM certificate for TLS termination at the NLB.
|
|
303
|
+
const cert = new Certificate(this, 'TlsCertificate', {
|
|
304
|
+
domainName: tcpTlsDomain,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// NLB requires a VPC. A dedicated two-AZ VPC keeps the NLB path
|
|
308
|
+
// isolated from the serverless (APIGW) path which has no VPC.
|
|
309
|
+
// Cast bypasses a CDK `exactOptionalPropertyTypes` friction between
|
|
310
|
+
// the concrete `Vpc` and the `IVpc` interface (optional vs required
|
|
311
|
+
// properties in the CDK jsii layer).
|
|
312
|
+
const vpc = new Vpc(this, 'NlbVpc', {
|
|
313
|
+
maxAzs: 2,
|
|
314
|
+
natGateways: 0,
|
|
315
|
+
}) as never;
|
|
316
|
+
|
|
317
|
+
// The NLB streaming Lambda — same entry, different handler export.
|
|
318
|
+
// Shares the bundled aws-adapter code (tree-shaken to the nlb-stream
|
|
319
|
+
// path by esbuild at synth time). Reuses the same DynamoDB tables,
|
|
320
|
+
// server identity, and MANAGEMENT_URL as the wss handler.
|
|
321
|
+
const nlbHandler = new NodejsFunction(this, 'IrcNlbHandler', {
|
|
322
|
+
runtime: Runtime.NODEJS_24_X,
|
|
323
|
+
entry: handlerEntry(),
|
|
324
|
+
handler: 'nlbStreamHandler',
|
|
325
|
+
bundling: {
|
|
326
|
+
externalModules: ['@aws-sdk/*'],
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Grant the NLB handler the same DynamoDB access as the wss handler.
|
|
331
|
+
for (const table of Object.values(tables)) {
|
|
332
|
+
table.grantReadWriteData(nlbHandler);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Inject the same table names, server identity, and management URL.
|
|
336
|
+
for (const logicalId of Object.keys(TABLE_DEFS)) {
|
|
337
|
+
const envName = `${logicalId.toUpperCase()}_TABLE`;
|
|
338
|
+
nlbHandler.addEnvironment(envName, prefixedTableName(environmentName, logicalId));
|
|
339
|
+
}
|
|
340
|
+
nlbHandler.addEnvironment('SERVER_NAME', serverName);
|
|
341
|
+
nlbHandler.addEnvironment('NETWORK_NAME', networkName);
|
|
342
|
+
nlbHandler.addEnvironment('MOTD', motd);
|
|
343
|
+
nlbHandler.addEnvironment('MANAGEMENT_URL', stage.callbackUrl);
|
|
344
|
+
|
|
345
|
+
const nlb = new NetworkLoadBalancer(this, 'IrcNlb', {
|
|
346
|
+
vpc,
|
|
347
|
+
internetFacing: true,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const targetGroup = new NetworkTargetGroup(this, 'IrcNlbTargetGroup', {
|
|
351
|
+
port: IRC_TLS_PORT,
|
|
352
|
+
protocol: Protocol.TCP,
|
|
353
|
+
targetType: TargetType.LAMBDA,
|
|
354
|
+
targets: [new LambdaTarget(nlbHandler)],
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
new NetworkListener(this, 'IrcNlbTlsListener', {
|
|
358
|
+
// Cast bypasses a CDK `exactOptionalPropertyTypes` friction between
|
|
359
|
+
// the concrete `NetworkLoadBalancer` and the `INetworkLoadBalancer`
|
|
360
|
+
// interface (optional vs required `securityGroups`).
|
|
361
|
+
loadBalancer: nlb as never,
|
|
362
|
+
port: IRC_TLS_PORT,
|
|
363
|
+
protocol: Protocol.TLS,
|
|
364
|
+
certificates: [{ certificateArn: cert.certificateArn }],
|
|
365
|
+
defaultTargetGroups: [targetGroup],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
new CfnOutput(this, 'TcpConnectUrl', {
|
|
369
|
+
description:
|
|
370
|
+
'TCP+TLS connect endpoint (irc+tls :6697). Connect via the NLB DNS name on port 6697.',
|
|
371
|
+
value: `${nlb.loadBalancerDnsName}:6697`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
235
375
|
new CfnOutput(this, 'ConnectUrl', {
|
|
236
376
|
description: 'WebSocket connect endpoint clients dial (wss).',
|
|
237
377
|
value: stage.url,
|