serverless-ircd 0.2.0 → 0.4.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 +258 -18
- package/README.md +72 -30
- package/apps/aws-stack/README.md +2 -2
- 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 +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- 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/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +11 -1
- package/apps/cf-worker/wrangler.toml +69 -19
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +11 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +46 -3
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- package/package.json +19 -16
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +124 -34
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +62 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +43 -2
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +120 -9
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +13 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
- package/packages/aws-adapter/tests/account-store.test.ts +279 -0
- 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/config-loader.test.ts +75 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +194 -47
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- 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 +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +40 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +62 -0
- package/packages/cf-adapter/src/connection-do.ts +181 -31
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +58 -8
- package/packages/cf-adapter/src/index.ts +11 -9
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- 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.test.ts +40 -0
- 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/worker/main.ts +9 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +8 -2
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- package/packages/irc-core/src/case-fold.ts +64 -0
- 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 +11 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +4 -3
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +26 -19
- 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/tagmsg.ts +205 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +84 -3
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +240 -7
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +60 -1
- package/packages/irc-core/tests/account-store.test.ts +131 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- 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/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +256 -19
- 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/tagmsg.test.ts +662 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +170 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- 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 +182 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +21 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +617 -1
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.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 +78 -13
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +132 -2
- 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 +9 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +79 -0
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/docs/AWS-Adapter-Architecture.md +0 -494
- package/docs/AWS-Deployment.md +0 -1107
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -1
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -440
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
package/docs/Observability.md
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Observability
|
|
2
|
-
|
|
3
|
-
The server ships a structured-logger port that every adapter uses to emit
|
|
4
|
-
JSON log lines carrying a per-request `traceId` and per-connection
|
|
5
|
-
`connectionId`. The canonical event names are the contract dashboards and
|
|
6
|
-
log queries are built against.
|
|
7
|
-
|
|
8
|
-
## Log contract
|
|
9
|
-
|
|
10
|
-
Every record is one physical line of JSON with at least:
|
|
11
|
-
|
|
12
|
-
| Field | Source |
|
|
13
|
-
|----------------|-------------------------------------------------|
|
|
14
|
-
| `ts` | ISO-8601 timestamp from the injected `Clock`. |
|
|
15
|
-
| `level` | `debug` / `info` / `warn` / `error`. |
|
|
16
|
-
| `msg` | Event name (see below). |
|
|
17
|
-
| `traceId` | Per-request id; fresh per inbound frame. |
|
|
18
|
-
| `connectionId` | The bound connection's id (`ConnId`). |
|
|
19
|
-
|
|
20
|
-
Additional structured fields appear per event. The `msg` catalogue:
|
|
21
|
-
|
|
22
|
-
| `msg` | Level | Emitted by | Notes |
|
|
23
|
-
|--------------------|-------|-------------------------|------------------------------------------------------|
|
|
24
|
-
| `frame.receive` | debug | `ConnectionActor` | One per parsed IRC line. Fields: `command`, `lines`. |
|
|
25
|
-
| `frame.parse-error`| warn | `ConnectionActor` | A line failed to parse. Fields: `line`, `token`. |
|
|
26
|
-
| `dispatch` | debug | `dispatch()` | Before interpreting an effect list. Fields: `effects` (array of tags), `histogram`. |
|
|
27
|
-
| `dispatch.error` | warn | `dispatch()` | A runtime method rejected. Fields: `effect`, `err`. |
|
|
28
|
-
|
|
29
|
-
Adapters may emit their own events (e.g. `local-cli listening`, `shutdown`)
|
|
30
|
-
following the same shape.
|
|
31
|
-
|
|
32
|
-
## Cloudflare Workers
|
|
33
|
-
|
|
34
|
-
The `apps/cf-worker/wrangler.toml` enables `[observability]` so every
|
|
35
|
-
`console.*` line emitted by the `ConsoleLogger` inside `ConnectionDO` is
|
|
36
|
-
captured by Workers Analytics.
|
|
37
|
-
|
|
38
|
-
- Tail the live feed: `pnpm --filter @serverless-ircd/cf-worker tail`
|
|
39
|
-
- Filter by connection: `connectionId = "<DO hex id>"`
|
|
40
|
-
- Filter by trace: `traceId = "<uuid>"`
|
|
41
|
-
- Errors only: `level = "error" or msg = "dispatch.error"`
|
|
42
|
-
|
|
43
|
-
Workers Analytics dashboards can be built on the Cloudflare dashboard
|
|
44
|
-
under **Workers & Pages → <Worker> → Observability**.
|
|
45
|
-
|
|
46
|
-
## AWS (Lambda + API Gateway WebSocket)
|
|
47
|
-
|
|
48
|
-
The Lambda handler writes to CloudWatch Logs via the global `console`.
|
|
49
|
-
Each invocation emits one JSON line per log record; the log group name
|
|
50
|
-
follows the standard `/aws/lambda/<function-name>` convention.
|
|
51
|
-
|
|
52
|
-
A starter CloudWatch dashboard is checked in at
|
|
53
|
-
`dashboards/cloudwatch-irc.json`. Provision it via:
|
|
54
|
-
|
|
55
|
-
```sh
|
|
56
|
-
aws cloudwatch put-dashboard \
|
|
57
|
-
--dashboard-name ServerlessIRCd-staging \
|
|
58
|
-
--dashboard-body file://dashboards/cloudwatch-irc.json
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
The dashboard variables (`API_ID`, `LAMBDA_NAME`, `LOG_GROUP_ARN`,
|
|
62
|
-
`AWS_REGION`) are templated by CloudWatch's dashboard variables feature —
|
|
63
|
-
set them once in the dashboard UI after import.
|
|
64
|
-
|
|
65
|
-
Sample CloudWatch Logs Insights query — count dispatched effects by tag
|
|
66
|
-
over the last hour:
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
fields @timestamp, msg, fields.histogram
|
|
70
|
-
| filter msg = "dispatch"
|
|
71
|
-
| sort @timestamp desc
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Local CLI
|
|
75
|
-
|
|
76
|
-
The local-cli server uses the same `ConsoleLogger` so the structured
|
|
77
|
-
stream is visible while developing:
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
{"ts":"2026-07-22T12:34:56.789Z","level":"debug","msg":"frame.receive","traceId":"…","connectionId":"…","command":"PRIVMSG","lines":1}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Filter with `jq`:
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
pnpm --filter local-cli start | jq 'select(.msg=="dispatch.error")'
|
|
87
|
-
```
|
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
# ServerlessIRCd — IRCv3 WebSocket Extension Plan
|
|
2
|
-
|
|
3
|
-
Bring the existing WebSocket entry points into compliance with the
|
|
4
|
-
[IRCv3 WebSocket support spec](https://ircv3.net/specs/extensions/websocket).
|
|
5
|
-
Today the server transports IRC over WebSocket but ignores the spec's
|
|
6
|
-
defining features: **subprotocol negotiation**, **one-line-per-message
|
|
7
|
-
framing**, the **510-byte message budget**, and **binary/text frame mode
|
|
8
|
-
distinction**. This plan closes those gaps across the pure core and all
|
|
9
|
-
three adapters (Cloudflare Workers, AWS, local CLI) plus the in-tree TCP
|
|
10
|
-
bridge.
|
|
11
|
-
|
|
12
|
-
> **Status:** planning. No code has landed. Seven tickets proposed in §7
|
|
13
|
-
> under a new **Phase 9 — IRCv3 WebSocket extension** heading; once
|
|
14
|
-
> ratified they will be appended to `tickets.md` / `progress.md` and
|
|
15
|
-
> supersede the corresponding sections here.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 1. Goals & non-goals
|
|
20
|
-
|
|
21
|
-
**Goals**
|
|
22
|
-
- Full compliance with the normative MUSTs of the IRCv3 WebSocket spec:
|
|
23
|
-
- Negotiate the `binary.ircv3.net` **and** `text.ircv3.net` subprotocols,
|
|
24
|
-
honoring the client's order of preference.
|
|
25
|
-
- One IRC line per WebSocket message, with no trailing `\r\n`.
|
|
26
|
-
- 510-byte default message budget (content; the server re-inserts
|
|
27
|
-
`\r\n` internally for any TCP relay / line accounting).
|
|
28
|
-
- Distinct binary vs text frame handling; never relay non-UTF-8 to a
|
|
29
|
-
text-mode client.
|
|
30
|
-
- Keep the change **inside the transport layer**. The hexagonal core
|
|
31
|
-
(`irc-core` reducers, `irc-server` `ConnectionActor`) stays
|
|
32
|
-
transport-agnostic; only framing/byte-boundary concerns move.
|
|
33
|
-
- Preserve backward compatibility: clients that do **not** negotiate a
|
|
34
|
-
subprotocol keep working via a legacy-tolerant path (the spec permits
|
|
35
|
-
this).
|
|
36
|
-
- Same TDD discipline as the rest of the monorepo: pure negotiation &
|
|
37
|
-
framing helpers unit-tested; per-adapter integration tests against a
|
|
38
|
-
real WS handshake.
|
|
39
|
-
|
|
40
|
-
**Non-goals (v1 of this work)**
|
|
41
|
-
- Non-UTF-8 *channel communities*. The core already reasons in JS strings
|
|
42
|
-
(UTF-8 at the boundary); we validate/replace lone surrogates but do not
|
|
43
|
-
introduce a raw-bytes code path through the reducers. Binary mode is
|
|
44
|
-
supported at the frame layer only.
|
|
45
|
-
- A second transport or SockJS gateway. `webircgateway` remains an
|
|
46
|
-
optional sidecar; not in scope here.
|
|
47
|
-
- WebSocket `Origin` allowlisting. That is CSWSH defense tracked separately
|
|
48
|
-
(PLAN-WEB-CLIENT §7, TICKET-070). This plan assumes an Origin check, if
|
|
49
|
-
added, layers on top of the upgrade handler untouched by subprotocol
|
|
50
|
-
work.
|
|
51
|
-
- Resumable sessions / `X-Connection-Id`. The upgrade handler will gain a
|
|
52
|
-
clean seam for future subprotocol-derived routing, but session resume is
|
|
53
|
-
out of scope.
|
|
54
|
-
|
|
55
|
-
**Design-for-later**
|
|
56
|
-
- The subprotocol selector returns the chosen mode as a value the adapters
|
|
57
|
-
stash on connection state. A future resumable-session ticket can ride
|
|
58
|
-
the same field instead of re-deriving it.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## 2. Current state (the compliance gap)
|
|
63
|
-
|
|
64
|
-
Audited against the spec. Findings, with file:line references:
|
|
65
|
-
|
|
66
|
-
| Spec requirement | Status | Evidence |
|
|
67
|
-
|---|---|---|
|
|
68
|
-
| Negotiate `binary.ircv3.net` / `text.ircv3.net` | **Missing** | `packages/cf-adapter/src/connection-do.ts:108-121` returns `101` with no protocol; `apps/cf-worker/src/worker.ts:45-49` checks only `Upgrade`. No `Sec-WebSocket-Protocol` / `subprotocol` strings anywhere. |
|
|
69
|
-
| One IRC line per WS message, no trailing CRLF | **Violated (legacy-tolerant instead)** | `packages/irc-server/src/actor.ts:150-159` `receiveTextFrame` splits every frame on `\r?\n` (`splitFrameLines`, line 454-457); outbound batches multiple lines into one frame joined by `\r\n` (`connection-do.ts:234`, `apps/local-cli/src/server.ts:507`, `packages/aws-adapter/src/handlers/default.ts:127`). |
|
|
70
|
-
| 510-byte message budget | **Off by 2** | `packages/irc-core/src/protocol/parser.ts:30` `MAX_INPUT_BYTES = 512`; `packages/irc-core/src/protocol/outbound.ts:27` `MAX_LINE_BYTES = 512` (defines `TRAILING_CRLF_LEN = 2` at line 30 but never subtracts it). |
|
|
71
|
-
| Binary vs text frame mode | **Not distinguished** | `connection-do.ts:128-129` silently `TextDecoder`-decodes `ArrayBuffer`; no `binaryType`, no mode tracking. |
|
|
72
|
-
| Don't relay non-UTF-8 to text clients | N/A until text mode exists | — |
|
|
73
|
-
| PING/PONG keepalive | **Already compliant** | CF alarms (`connection-do.ts:185-204`); AWS EventBridge (`ping-checker.ts`). |
|
|
74
|
-
| Impose message-size limits (security SHOULD) | **Present**, wrong threshold | 512 caps exist; spec wants 510 for WS. |
|
|
75
|
-
|
|
76
|
-
The transport itself works today (CF Hibernating WebSockets, AWS API
|
|
77
|
-
Gateway, local `ws`). What's missing is the *spec framing* layered on top.
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## 3. Architecture
|
|
82
|
-
|
|
83
|
-
WebSocket framing is a **transport-boundary** concern, so the work fans
|
|
84
|
-
out across the hexagon but the *logic* stays pure and central:
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
┌──────────────────────────────────────────────────────────────────┐
|
|
88
|
-
│ packages/irc-core (pure, no cloud deps) │
|
|
89
|
-
│ ws-subprotocol.ts parseSecWsProtocolOffers / selectSubproto │
|
|
90
|
-
│ ws-framing.ts frameMode, stripTrailingCrLf, │
|
|
91
|
-
│ splitWsFrame (legacy), enforce 510 budget │
|
|
92
|
-
│ protocol/outbound.ts WS-aware line-limit (510) + lone- │
|
|
93
|
-
│ surrogate sanitization for text mode │
|
|
94
|
-
└──────────────────────────────────────────────────────────────────┘
|
|
95
|
-
▲ used by
|
|
96
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
97
|
-
│ packages/irc-server ConnectionActor │
|
|
98
|
-
│ receiveFrame(text|bytes, mode) mode-aware entry: spec = │
|
|
99
|
-
│ one line; legacy = split │
|
|
100
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
101
|
-
▲ ▲ ▲
|
|
102
|
-
┌──────────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
|
|
103
|
-
│ cf-adapter │ │ aws-adapter │ │ local-cli │
|
|
104
|
-
│ ConnectionDO.fetch: │ │ $connect: echo │ │ ws handleProtocols; │
|
|
105
|
-
│ read header, echo │ │ Sec-WebSocket- │ │ per-conn mode; │
|
|
106
|
-
│ subprotocol, store │ │ Protocol via CDK │ │ per-message send │
|
|
107
|
-
│ mode; hibernation │ │ integration resp; │ │ │
|
|
108
|
-
│ tag; per-message │ │ Connections.mode; │ │ │
|
|
109
|
-
│ send; binary/text │ │ per-message send │ │ │
|
|
110
|
-
└──────────────────────┘ └─────────────────────┘ └──────────────────────┘
|
|
111
|
-
▲
|
|
112
|
-
┌─────────────────────────┐
|
|
113
|
-
│ tools/tcp-ws-forwarder │ offer binary.ircv3.net upstream;
|
|
114
|
-
│ │ strip/append CRLF at the TCP seam
|
|
115
|
-
└─────────────────────────┘
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
**Key invariant:** the *reducers never change*. Framing (how bytes become
|
|
119
|
-
`IrcMessage`s and how `Effect`s become bytes) is the only thing touched.
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## 4. Design decisions
|
|
124
|
-
|
|
125
|
-
### 4.1 Subprotocol selection (pure)
|
|
126
|
-
|
|
127
|
-
A pure helper set in `irc-core`:
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
export const WS_SUBPROTO_BINARY = 'binary.ircv3.net';
|
|
131
|
-
export const WS_SUBPROTO_TEXT = 'text.ircv3.net';
|
|
132
|
-
export type WsSubprotocol = typeof WS_SUBPROTO_BINARY | typeof WS_SUBPROTO_TEXT;
|
|
133
|
-
|
|
134
|
-
/** Parse a `Sec-WebSocket-Protocol` header (comma list, trimmed). */
|
|
135
|
-
export function parseSecWsProtocolOffers(header: string | null): string[];
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Pick the first offered protocol we support, in client-preference order.
|
|
139
|
-
* Returns null if none match (caller MAY still accept the connection per spec).
|
|
140
|
-
*/
|
|
141
|
-
export function selectSubprotocol(offers: string[]): WsSubprotocol | null;
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Selection order is **the client's order** (spec: "respecting the client's
|
|
145
|
-
order of preference"). We accept the first of `[binary.ircv3.net,
|
|
146
|
-
text.ircv3.net]` the client listed. If the client offers neither, return
|
|
147
|
-
`null` — the adapter still upgrades (legacy path) per the spec's MAY.
|
|
148
|
-
|
|
149
|
-
### 4.2 Frame mode on connection state
|
|
150
|
-
|
|
151
|
-
Adapters store the negotiated mode on per-connection state. We do **not**
|
|
152
|
-
thread it through the core reducers (they're mode-blind); only the
|
|
153
|
-
framing entry point and the outbound serializer consult it.
|
|
154
|
-
|
|
155
|
-
- CF: hibernation tag via `acceptWebSocket(ws, [tag])` where
|
|
156
|
-
`tag = "ws:" + mode`; recovered in `webSocketMessage` via
|
|
157
|
-
`ws.deserialize`-equivalent tag read. Persisted as a field in DO state
|
|
158
|
-
for the outbound path.
|
|
159
|
-
- AWS: new `mode` column on the `Connections` table (default `null` =
|
|
160
|
-
legacy).
|
|
161
|
-
- Local CLI: a `Map<WebSocket, WsSubprotocol | null>` on the server.
|
|
162
|
-
|
|
163
|
-
### 4.3 Inbound framing — spec mode vs legacy mode
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
export type WsFrameMode = 'spec-text' | 'spec-binary' | 'legacy';
|
|
167
|
-
|
|
168
|
-
/** One frame -> one line in spec mode; tolerant split in legacy mode. */
|
|
169
|
-
export function frameToLines(frame: string, mode: WsFrameMode): string[];
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
- **spec-text / spec-binary:** the frame *is* the line. Strip an optional
|
|
173
|
-
single trailing `\r\n`/`\n` (defensive; clients SHOULD NOT send it), then
|
|
174
|
-
return `[frame]` if non-empty else `[]`. Never split on embedded
|
|
175
|
-
newlines.
|
|
176
|
-
- **legacy:** current `splitFrameLines` behavior (`/\r?\n/`), preserved
|
|
177
|
-
verbatim for clients that never negotiated.
|
|
178
|
-
|
|
179
|
-
`ConnectionActor` gains `receiveFrame(payload: string, mode)`; existing
|
|
180
|
-
`receiveTextFrame` becomes a thin caller passing `'legacy'` so all current
|
|
181
|
-
call sites and tests keep their behavior until updated.
|
|
182
|
-
|
|
183
|
-
### 4.4 Outbound framing — one message per line in spec mode
|
|
184
|
-
|
|
185
|
-
Today adapters batch `lines.map(l => l.text).join('\r\n') + '\r\n'` into a
|
|
186
|
-
single `ws.send`. In **spec mode** each outbound IRC line is its own WS
|
|
187
|
-
message with **no** trailing CRLF:
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
// spec mode
|
|
191
|
-
for (const line of outboundLines) ws.send(line.text); // one msg each, no CRLF
|
|
192
|
-
// legacy mode (unchanged)
|
|
193
|
-
ws.send(outboundLines.map(l => l.text).join('\r\n') + '\r\n');
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
This is the largest mechanical change (three adapters + their tests), but
|
|
197
|
-
localized to the delivery functions.
|
|
198
|
-
|
|
199
|
-
### 4.5 Size budget — 510 for WS, 512 for TCP
|
|
200
|
-
|
|
201
|
-
Introduce `MAX_WS_MESSAGE_BYTES = 510` alongside the existing 512 TCP cap.
|
|
202
|
-
The WS inbound path enforces 510 on the message (no CRLF to discount);
|
|
203
|
-
`enforceLineLimit` gains a mode-aware budget so spec-mode clients get the
|
|
204
|
-
correct outbound ceiling. TCP and legacy-WS paths are unchanged.
|
|
205
|
-
|
|
206
|
-
### 4.6 Binary vs text handling
|
|
207
|
-
|
|
208
|
-
- **spec-binary (`binary.ircv3.net`):** frames are `ArrayBuffer`. CF
|
|
209
|
-
`webSocketMessage` already receives `string | ArrayBuffer`; we pass
|
|
210
|
-
bytes through `TextDecoder` (our core is string-based) **but only after**
|
|
211
|
-
validating. Binary mode is the "any valid IRC line" mode; for our
|
|
212
|
-
UTF-8-string core this collapses to the same decode as text, with the
|
|
213
|
-
difference being that we *accept* a binary frame without complaint.
|
|
214
|
-
- **spec-text (`text.ircv3.net`):** frames are `string`. If a binary frame
|
|
215
|
-
arrives on a text-mode connection, reject per spec (close 1003 /
|
|
216
|
-
unsupported data) rather than silently decoding.
|
|
217
|
-
- **Non-UTF-8 to text clients:** our outbound is always JS strings → valid
|
|
218
|
-
UTF-8. The residual risk is *lone surrogates* (unpaired halves of
|
|
219
|
-
surrogate pairs, which `TextEncoder` rejects). `sanitizeForTextMode`
|
|
220
|
-
replaces lone surrogates with U+FFFD before send on text-mode sockets.
|
|
221
|
-
|
|
222
|
-
### 4.7 Backward compatibility
|
|
223
|
-
|
|
224
|
-
The web client (PLAN-WEB-CLIENT) and `tcp-ws-forwarder` currently rely on
|
|
225
|
-
the legacy `\r\n`-batched framing. Because un-negotiated connections fall
|
|
226
|
-
into `'legacy'` mode, **nothing breaks** until each client opts in by
|
|
227
|
-
offering a subprotocol. The forwarder is updated in the same phase so the
|
|
228
|
-
in-tree bridge immediately exercises the spec path.
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
## 5. Component placement
|
|
233
|
-
|
|
234
|
-
No new workspace packages. Changes are additions/edits within existing
|
|
235
|
-
packages:
|
|
236
|
-
|
|
237
|
-
```
|
|
238
|
-
packages/irc-core/src/
|
|
239
|
-
├── ws-subprotocol.ts NEW — parseSecWsProtocolOffers, selectSubprotocol
|
|
240
|
-
├── ws-framing.ts NEW — WsFrameMode, frameToLines, stripTrailingCrLf,
|
|
241
|
-
│ MAX_WS_MESSAGE_BYTES, sanitizeForTextMode
|
|
242
|
-
└── protocol/outbound.ts EDIT — mode-aware enforceLineLimit (510 vs 512)
|
|
243
|
-
packages/irc-core/tests/
|
|
244
|
-
├── ws-subprotocol.test.ts NEW
|
|
245
|
-
└── ws-framing.test.ts NEW
|
|
246
|
-
|
|
247
|
-
packages/irc-server/src/actor.ts EDIT — receiveFrame(text, mode);
|
|
248
|
-
receiveTextFrame delegates
|
|
249
|
-
packages/irc-server/tests/actor.*.test.ts EDIT — add spec-mode cases
|
|
250
|
-
|
|
251
|
-
packages/cf-adapter/src/connection-do.ts EDIT — upgrade negotiation, mode
|
|
252
|
-
tag/storage, per-msg send,
|
|
253
|
-
binary/text branch
|
|
254
|
-
packages/aws-adapter/src/handlers/connect.ts EDIT — read header, persist mode
|
|
255
|
-
packages/aws-adapter/src/handlers/default.ts EDIT — per-message send
|
|
256
|
-
apps/aws-stack/src/aws-stack.ts EDIT — integration response
|
|
257
|
-
Sec-WebSocket-Protocol header
|
|
258
|
-
apps/local-cli/src/server.ts EDIT — handleProtocols, mode map,
|
|
259
|
-
per-message send
|
|
260
|
-
tools/tcp-ws-forwarder/src/forwarder.ts EDIT — offer binary subproto,
|
|
261
|
-
framing translation
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
---
|
|
265
|
-
|
|
266
|
-
## 6. Testing strategy
|
|
267
|
-
|
|
268
|
-
- **Pure units (Red-first):** `ws-subprotocol.test.ts` and
|
|
269
|
-
`ws-framing.test.ts` drive the helpers — header parsing edge cases
|
|
270
|
-
(whitespace, duplicates, empty), preference ordering, spec-vs-legacy
|
|
271
|
-
line splitting, 510-boundary, lone-surrogate replacement.
|
|
272
|
-
- **Actor unit:** extend `actor.*.test.ts` to call `receiveFrame(...,
|
|
273
|
-
'spec-text')` and assert one-line-per-message semantics; keep existing
|
|
274
|
-
`receiveTextFrame` tests green (legacy).
|
|
275
|
-
- **CF integration (`vitest-pool-workers` / Miniflare):** open a real WS
|
|
276
|
-
to `ConnectionDO` offering `text.ircv3.net`, assert the 101 response
|
|
277
|
-
echoes the protocol, send `NICK`/`USER` as separate CRLF-less messages,
|
|
278
|
-
assert registration replies arrive one-per-message. A second case
|
|
279
|
-
offers neither protocol and asserts legacy batching still works.
|
|
280
|
-
- **AWS:** unit-test the connect handler's header→mode mapping and the CDK
|
|
281
|
-
integration-response header; local-stack smoke optional.
|
|
282
|
-
- **Determinism:** all of the above are hermetic except an optional real-
|
|
283
|
-
browser check, which stays behind a `--e2e` flag like the web client
|
|
284
|
-
plan.
|
|
285
|
-
|
|
286
|
-
---
|
|
287
|
-
|
|
288
|
-
## 7. Proposed tickets
|
|
289
|
-
|
|
290
|
-
Seven new tickets under a new **Phase 9 — IRCv3 WebSocket extension**
|
|
291
|
-
heading in `tickets.md` / `progress.md`. Each follows the existing
|
|
292
|
-
Red → Green → Refactor TDD outline.
|
|
293
|
-
|
|
294
|
-
### TICKET-073 — Core: WebSocket subprotocol negotiation helpers
|
|
295
|
-
- **Phase:** 9
|
|
296
|
-
- **Points:** 2
|
|
297
|
-
- **Dependencies:** —
|
|
298
|
-
- **Description:** Add `packages/irc-core/src/ws-subprotocol.ts` with
|
|
299
|
-
`WS_SUBPROTO_BINARY`/`WS_SUBPROTO_TEXT` constants, the `WsSubprotocol`
|
|
300
|
-
type, `parseSecWsProtocolOffers(header)`, and
|
|
301
|
-
`selectSubprotocol(offers)` honoring client-preference order, returning
|
|
302
|
-
`null` when neither is offered. Pure, zero deps.
|
|
303
|
-
- **Acceptance Criteria:**
|
|
304
|
-
- `parseSecWsProtocolOffers('text.ircv3.net, binary.ircv3.net')`
|
|
305
|
-
yields `['text.ircv3.net','binary.ircv3.net']` (whitespace-tolerant).
|
|
306
|
-
- `selectSubprotocol` returns the first supported entry in offer order.
|
|
307
|
-
- Offering only unsupported protocols returns `null`.
|
|
308
|
-
- `null`/empty header → `[]` offers → `null` selection.
|
|
309
|
-
- **TDD Outline:** Red on preference-order test; green. Red on duplicate/
|
|
310
|
-
whitespace cases; green. Red on no-match returns null; green.
|
|
311
|
-
|
|
312
|
-
### TICKET-074 — Core: WS frame framing & 510-byte budget
|
|
313
|
-
- **Phase:** 9
|
|
314
|
-
- **Points:** 3
|
|
315
|
-
- **Dependencies:** TICKET-073
|
|
316
|
-
- **Description:** Add `packages/irc-core/src/ws-framing.ts`: `WsFrameMode`
|
|
317
|
-
(`'spec-text' | 'spec-binary' | 'legacy'`), `MAX_WS_MESSAGE_BYTES = 510`,
|
|
318
|
-
`stripTrailingCrLf`, `frameToLines(frame, mode)` (spec = single line;
|
|
319
|
-
legacy = current `splitFrameLines` behavior), and
|
|
320
|
-
`sanitizeForTextMode(str)` replacing lone surrogates with U+FFFD. Move
|
|
321
|
-
the existing `splitFrameLines` body here (or re-export) so legacy mode
|
|
322
|
-
is literally the current code. Make `enforceLineLimit` in `outbound.ts`
|
|
323
|
-
budget-aware (510 in spec modes, 512 otherwise).
|
|
324
|
-
- **Acceptance Criteria:**
|
|
325
|
-
- `frameToLines('NICK bob\r\n', 'spec-text')` → `['NICK bob']` (one
|
|
326
|
-
line; CRLF stripped; not split).
|
|
327
|
-
- Embedded `\n` in a spec-mode frame stays a single line.
|
|
328
|
-
- `frameToLines('a\r\nb', 'legacy')` → `['a','b']` (unchanged behavior).
|
|
329
|
-
- WS inbound rejects a 511-byte message; accepts 510.
|
|
330
|
-
- `sanitizeForTextMode` turns a lone surrogate into U+FFFD.
|
|
331
|
-
- **TDD Outline:** Red per bullet above; minimal green; refactor shared
|
|
332
|
-
strip logic.
|
|
333
|
-
|
|
334
|
-
### TICKET-075 — CF adapter: negotiate subprotocol + per-message framing
|
|
335
|
-
- **Phase:** 9
|
|
336
|
-
- **Points:** 3
|
|
337
|
-
- **Dependencies:** TICKET-073, TICKET-074
|
|
338
|
-
- **Description:** In `ConnectionDO.fetch` read `Sec-WebSocket-Protocol`,
|
|
339
|
-
run `selectSubprotocol`, echo the chosen protocol back in the `101`
|
|
340
|
-
response headers, and tag the hibernated socket (`acceptWebSocket(ws,
|
|
341
|
-
[\`ws:${mode}\`])`). Derive `WsFrameMode` in `webSocketMessage` (binary
|
|
342
|
-
frame on a `spec-text` connection → close 1003). Store the mode in DO
|
|
343
|
-
state for the outbound path. Change outbound delivery to one
|
|
344
|
-
`ws.send(line)` per IRC line in spec mode (no CRLF); keep legacy
|
|
345
|
-
batching otherwise. Apply `sanitizeForTextMode` on `spec-text` sends.
|
|
346
|
-
- **Acceptance Criteria:**
|
|
347
|
-
- 101 response to an offer of `text.ircv3.net` includes
|
|
348
|
-
`Sec-WebSocket-Protocol: text.ircv3.net`.
|
|
349
|
-
- A client offering both gets the first-listed supported protocol.
|
|
350
|
-
- An un-negotiated client still connects (legacy mode) and existing CF
|
|
351
|
-
WS tests stay green.
|
|
352
|
-
- Registration replies to a spec-mode client arrive as N separate WS
|
|
353
|
-
messages with no trailing CRLF.
|
|
354
|
-
- Binary frame on a text-mode socket closes 1003.
|
|
355
|
-
- **TDD Outline:** Red on a Miniflare WS asserting the echoed header;
|
|
356
|
-
green. Red asserting per-message send (count messages); green. Red on
|
|
357
|
-
binary-on-text close; green.
|
|
358
|
-
|
|
359
|
-
### TICKET-076 — Local CLI adapter: `handleProtocols` + per-message framing
|
|
360
|
-
- **Phase:** 9
|
|
361
|
-
- **Points:** 2
|
|
362
|
-
- **Dependencies:** TICKET-073, TICKET-074
|
|
363
|
-
- **Description:** Give `apps/local-cli/src/server.ts`'s `WebSocketServer`
|
|
364
|
-
a `handleProtocols` callback backed by the core selector; keep a
|
|
365
|
-
`Map<WebSocket, WsSubprotocol | null>` for mode. Route inbound through
|
|
366
|
-
the actor's `receiveFrame`; switch the `ws.send` delivery to per-message
|
|
367
|
-
in spec mode. Un-negotiated clients keep today's behavior.
|
|
368
|
-
- **Acceptance Criteria:**
|
|
369
|
-
- `wscat -s text.ircv3.net` against the local server negotiates the
|
|
370
|
-
protocol and exchanges one-line-per-message frames.
|
|
371
|
-
- Existing local-cli integration tests pass unchanged (legacy path).
|
|
372
|
-
- **TDD Outline:** Red on a `ws` client asserting the echoed subprotocol
|
|
373
|
-
and one-message-per-line; green.
|
|
374
|
-
|
|
375
|
-
### TICKET-077 — AWS adapter: subprotocol echo + per-message framing
|
|
376
|
-
- **Phase:** 9
|
|
377
|
-
- **Points:** 3
|
|
378
|
-
- **Dependencies:** TICKET-073, TICKET-074
|
|
379
|
-
- **Description:** API Gateway lets the integration choose the
|
|
380
|
-
subprotocol via the `$connect` route's integration-response header
|
|
381
|
-
`Sec-WebSocket-Protocol`. Read the offered header in `connect.ts`, run
|
|
382
|
-
the selector, persist the chosen `mode` (new `Connections` column,
|
|
383
|
-
default null = legacy), and emit the header in the CDK
|
|
384
|
-
integration-response (`apps/aws-stack/src/aws-stack.ts`). In
|
|
385
|
-
`default.ts`, deliver each outbound line as its own `postToConnection`
|
|
386
|
-
in spec mode.
|
|
387
|
-
- **Acceptance Criteria:**
|
|
388
|
-
- A `$connect` offering `binary.ircv3.net` gets the protocol echoed and
|
|
389
|
-
a non-null `mode` row.
|
|
390
|
-
- `$default` replies to a spec-mode connection arrive one per
|
|
391
|
-
`postToConnection`.
|
|
392
|
-
- Legacy connections (no offer) are unaffected.
|
|
393
|
-
- **TDD Outline:** Red on connect-handler unit asserting header→mode;
|
|
394
|
-
green. Red on CDK template asserting the integration-response header;
|
|
395
|
-
green. Red on default-handler per-message delivery; green.
|
|
396
|
-
|
|
397
|
-
### TICKET-078 — tcp-ws-forwarder: negotiate `binary.ircv3.net` upstream
|
|
398
|
-
- **Phase:** 9
|
|
399
|
-
- **Points:** 2
|
|
400
|
-
- **Dependencies:** TICKET-075
|
|
401
|
-
- **Description:** When `tools/tcp-ws-forwarder` dials the IRCd WebSocket,
|
|
402
|
-
offer `['binary.ircv3.net', 'text.ircv3.net']`. Translate framing at the
|
|
403
|
-
TCP seam: each TCP IRC line (CRLF-terminated) → one WS message **with
|
|
404
|
-
the CRLF stripped**; each inbound WS message → one TCP line **with CRLF
|
|
405
|
-
re-appended**. This makes the in-tree bridge exercise the spec path
|
|
406
|
-
end-to-end against the CF worker.
|
|
407
|
-
- **Acceptance Criteria:**
|
|
408
|
-
- A real IRC TCP client (e.g. weechat/irssi) through the forwarder
|
|
409
|
-
completes registration against a spec-mode worker.
|
|
410
|
-
- Forwarder unit tests assert CRLF strip on send and re-append on
|
|
411
|
-
receive.
|
|
412
|
-
- **TDD Outline:** Red on send-translation (no trailing CRLF in WS msg);
|
|
413
|
-
green. Red on receive-translation (CRLF appended to TCP); green.
|
|
414
|
-
|
|
415
|
-
### TICKET-079 — Integration: spec-compliance test + docs note
|
|
416
|
-
- **Phase:** 9
|
|
417
|
-
- **Points:** 2
|
|
418
|
-
- **Dependencies:** TICKET-075, TICKET-076, TICKET-077
|
|
419
|
-
- **Description:** Add a `vitest-pool-workers` integration test that
|
|
420
|
-
negotiates `text.ircv3.net` against `ConnectionDO` end-to-end and
|
|
421
|
-
asserts the full spec contract (echoed header, one-line-per-message
|
|
422
|
-
both directions, 510 budget, legacy fallback still works). Add a short
|
|
423
|
-
"WebSocket (IRCv3)" section to the README listing supported
|
|
424
|
-
subprotocols and the legacy fallback behavior.
|
|
425
|
-
- **Acceptance Criteria:**
|
|
426
|
-
- The integration test is green and runs in the normal `pnpm test`
|
|
427
|
-
suite (no `--e2e` flag needed — it's hermetic against Miniflare).
|
|
428
|
-
- README documents `binary.ircv3.net` / `text.ircv3.net` support.
|
|
429
|
-
- **TDD Outline:** Red on the end-to-end spec assertions; green as the
|
|
430
|
-
dependency tickets land. Docs reviewed by reading the rendered README.
|
|
431
|
-
|
|
432
|
-
---
|
|
433
|
-
|
|
434
|
-
## 8. Risks
|
|
435
|
-
|
|
436
|
-
| Risk | Mitigation |
|
|
437
|
-
|---|---|
|
|
438
|
-
| Per-message send (N `ws.send`/`postToConnection` calls) raises CF/AWS cost or latency vs one batched frame | Measure in TICKET-075/077; batching IRC lines into one message is itself spec-non-compliant, so the cost is accepted. Cap bursts if needed via the existing rate limiter. |
|
|
439
|
-
| Hibernation tag API (`acceptWebSocket(ws, [tag])`) surface drift across runtime versions | Confirm against current `compatibility_date` in `apps/cf-worker/wrangler.toml`; fall back to persisting mode in DO state if the tag read is unavailable. |
|
|
440
|
-
| Existing clients (Kiwi via PLAN-WEB-CLIENT, forwarder) break if legacy mode regresses | Legacy mode is literally the moved `splitFrameLines`/batched-send code; keep its tests unchanged and gate spec behavior strictly on a negotiated subprotocol. |
|
|
441
|
-
| API Gateway subprotocol echo has regional/CDK quirks | Validate on a real stack before merging TICKET-077; keep legacy (no-offer) path as the safe default. |
|
|
442
|
-
| 510 vs 512 boundary rejects a currently-accepted 511-byte legacy line | The 510 budget applies **only** to spec-mode WS; TCP and legacy-WS keep 512. No regression for existing traffic. |
|
|
443
|
-
| Lone-surrogate sanitization alters message content | Applies to text mode only and matches the spec's U+FFFD guidance; binary-mode clients are unaffected. |
|
|
444
|
-
|
|
445
|
-
---
|
|
446
|
-
|
|
447
|
-
## 9. Out of scope for v1 of this work
|
|
448
|
-
|
|
449
|
-
- Raw-bytes / non-UTF-8 channel communities through the reducers. Binary
|
|
450
|
-
mode is honored at the frame layer; the core stays string-based.
|
|
451
|
-
- A new transport (SockJS / `webircgateway` integration). The gateway
|
|
452
|
-
remains an optional sidecar.
|
|
453
|
-
- WebSocket `Origin` allowlisting (tracked in PLAN-WEB-CLIENT
|
|
454
|
-
TICKET-070).
|
|
455
|
-
- Resumable sessions / `X-Connection-Id` routing.
|
|
456
|
-
- Per-message compression (permessage-deflate) — a Cloudflare/AWS platform
|
|
457
|
-
concern, not an IRCv3 WebSocket spec item.
|
|
458
|
-
|
|
459
|
-
---
|
|
460
|
-
|
|
461
|
-
## 10. Deliverables checklist (definition of done)
|
|
462
|
-
|
|
463
|
-
- [ ] `ws-subprotocol.ts` + `ws-framing.ts` land in `irc-core` with unit
|
|
464
|
-
tests green and ≥90% coverage.
|
|
465
|
-
- [ ] `ConnectionActor.receiveFrame` supports spec/legacy modes; all
|
|
466
|
-
existing actor tests pass.
|
|
467
|
-
- [ ] CF, AWS, and local-cli adapters negotiate a subprotocol, echo it,
|
|
468
|
-
and send one message per IRC line in spec mode; legacy path unchanged.
|
|
469
|
-
- [ ] `tcp-ws-forwarder` offers the binary subprotocol and translates
|
|
470
|
-
framing at the TCP seam.
|
|
471
|
-
- [ ] Hermetic Miniflare integration test asserts the full spec contract.
|
|
472
|
-
- [ ] README documents IRCv3 WebSocket support and the legacy fallback.
|
|
473
|
-
- [ ] All new tickets green in `progress.md`; PLAN §2 gap table updated to
|
|
474
|
-
show compliance.
|
|
475
|
-
|
|
476
|
-
---
|
|
477
|
-
|
|
478
|
-
## 11. Open questions
|
|
479
|
-
|
|
480
|
-
- Should un-negotiated connections default to **text** mode (stricter,
|
|
481
|
-
simpler) instead of legacy-tolerant? Lean toward legacy for max compat;
|
|
482
|
-
revisit once the web client adopts subprotocol negotiation.
|
|
483
|
-
- Do we want to advertise WS subprotocol support via an IRCv3 capability
|
|
484
|
-
(there isn't a standard one) or purely at the HTTP layer? Lean HTTP-only
|
|
485
|
-
— the spec defines no IRC-level CAP for this.
|
|
486
|
-
- For AWS, is the per-message `postToConnection` cost acceptable at scale,
|
|
487
|
-
or do we need a spec-compliant batching strategy (e.g. one message per
|
|
488
|
-
IRC line but coalesced within a single Lambda response where API
|
|
489
|
-
Gateway permits)? Defer until measured.
|