serverless-ircd 0.9.0 → 0.11.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 +28 -0
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +35 -9
- package/.github/workflows/deploy-cf.yml +40 -14
- package/CHANGELOG.md +594 -0
- package/README.md +286 -60
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +118 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +398 -67
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +714 -105
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -3
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +267 -87
- package/apps/cf-tcp-container/src/main.ts +22 -7
- package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
- package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
- package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
- package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
- package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
- package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
- package/apps/cf-tcp-container/wrangler.toml +18 -14
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/src/worker.ts +77 -5
- package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
- package/apps/cf-worker/tests/smoke.test.ts +4 -0
- package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
- package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
- package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
- package/apps/cf-worker/wrangler.test.toml +15 -1
- package/apps/cf-worker/wrangler.toml +95 -77
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +14 -2
- package/apps/local-cli/src/line-scanner.ts +26 -0
- package/apps/local-cli/src/server.ts +44 -19
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/docs/AWS-Deployment.md +793 -118
- package/docs/AWS-TCP-Deployment.md +57 -47
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +95 -114
- package/docs/Cloudflare-TCP-Deployment.md +160 -101
- package/docs/Release-Process.md +27 -23
- package/docs/SASL-EXTERNAL.md +175 -0
- package/docs/Services.md +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/admission.ts +28 -13
- package/packages/aws-adapter/src/aws-runtime.ts +30 -3
- package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
- package/packages/aws-adapter/src/config-loader.ts +153 -8
- package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
- package/packages/aws-adapter/src/handlers/connect.ts +73 -1
- package/packages/aws-adapter/src/handlers/default.ts +279 -123
- package/packages/aws-adapter/src/handlers/index.ts +98 -25
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +23 -0
- package/packages/aws-adapter/src/tables.ts +11 -12
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
- package/packages/aws-adapter/tests/connect.test.ts +323 -3
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
- package/packages/aws-adapter/tests/handlers.test.ts +174 -12
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +100 -10
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +406 -116
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +105 -26
- package/packages/cf-adapter/src/env.ts +99 -10
- package/packages/cf-adapter/src/index.ts +17 -7
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
- package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
- package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
- package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/cf-adapter/tests/worker/main.ts +4 -0
- 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 +25 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/cap.ts +10 -2
- package/packages/irc-core/src/commands/chanserv.ts +117 -14
- package/packages/irc-core/src/commands/chathistory.ts +13 -5
- package/packages/irc-core/src/commands/hostserv.ts +84 -8
- package/packages/irc-core/src/commands/index.ts +2 -1
- package/packages/irc-core/src/commands/invite.ts +1 -7
- package/packages/irc-core/src/commands/join.ts +1 -16
- package/packages/irc-core/src/commands/kick.ts +1 -8
- package/packages/irc-core/src/commands/list.ts +1 -8
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/mode.ts +1 -8
- package/packages/irc-core/src/commands/multiline.ts +4 -10
- package/packages/irc-core/src/commands/names.ts +53 -13
- package/packages/irc-core/src/commands/nickserv.ts +161 -11
- package/packages/irc-core/src/commands/oper.ts +361 -8
- package/packages/irc-core/src/commands/part.ts +4 -10
- package/packages/irc-core/src/commands/privmsg.ts +8 -4
- package/packages/irc-core/src/commands/registration.ts +148 -4
- package/packages/irc-core/src/commands/sasl.ts +154 -46
- package/packages/irc-core/src/commands/topic.ts +10 -12
- package/packages/irc-core/src/commands/who.ts +1 -8
- package/packages/irc-core/src/config.ts +424 -25
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/effects.ts +24 -0
- package/packages/irc-core/src/flood-control.ts +10 -10
- package/packages/irc-core/src/frame-rate-limit.ts +82 -0
- package/packages/irc-core/src/index.ts +9 -0
- package/packages/irc-core/src/oper-hashing.ts +43 -0
- package/packages/irc-core/src/oper-lockout.ts +87 -0
- package/packages/irc-core/src/ports.ts +529 -190
- package/packages/irc-core/src/protocol/bytes.ts +65 -0
- package/packages/irc-core/src/protocol/channel-name.ts +37 -0
- package/packages/irc-core/src/protocol/index.ts +12 -1
- package/packages/irc-core/src/protocol/outbound.ts +43 -10
- package/packages/irc-core/src/protocol/parser.ts +79 -10
- package/packages/irc-core/src/state/connection.ts +13 -0
- package/packages/irc-core/src/types.ts +266 -23
- package/packages/irc-core/src/ws-framing.ts +5 -4
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/bytes.test.ts +89 -0
- package/packages/irc-core/tests/certfp.test.ts +117 -0
- package/packages/irc-core/tests/commands/cap.test.ts +76 -2
- package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
- package/packages/irc-core/tests/commands/join.test.ts +78 -1
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
- package/packages/irc-core/tests/commands/oper.test.ts +574 -1
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +602 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
- package/packages/irc-core/tests/config.test.ts +534 -2
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/flood-control.test.ts +29 -1
- package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
- package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
- package/packages/irc-core/tests/outbound.test.ts +148 -0
- package/packages/irc-core/tests/parser.test.ts +287 -5
- package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
- package/packages/irc-core/tests/ports.test.ts +170 -7
- package/packages/irc-core/tests/services-store.test.ts +567 -1
- package/packages/irc-core/tests/ws-framing.test.ts +45 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +123 -22
- package/packages/irc-server/src/dispatch.ts +1 -0
- package/packages/irc-server/src/index.ts +7 -0
- package/packages/irc-server/src/redact.ts +159 -0
- package/packages/irc-server/src/runtime.ts +14 -0
- package/packages/irc-server/src/transport.ts +28 -1
- package/packages/irc-server/tests/actor.test.ts +563 -54
- package/packages/irc-server/tests/dispatch.test.ts +31 -0
- package/packages/irc-server/tests/redact.test.ts +198 -0
- package/packages/irc-server/tests/runtime.test.ts +2 -0
- package/packages/irc-server/tests/transport.test.ts +66 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +2 -2
- package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
- package/tools/ci-hardening/src/cf-deploy.ts +118 -0
- package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
- package/tools/ci-hardening/src/env-var-drift.ts +192 -0
- package/tools/ci-hardening/src/hostname-guard.ts +11 -0
- package/tools/ci-hardening/src/index.ts +19 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
- package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
- package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
- package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/ci-hardening/vitest.config.ts +5 -1
- package/tools/hash-oper-cred.ts +85 -0
- package/tools/load-test/package.json +1 -1
- package/tools/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,600 @@ For the release process itself — versioning policy, pre-release checklist,
|
|
|
10
10
|
cutting a tag, rolling back — see [`docs/Release-Process.md`](docs/Release-Process.md).
|
|
11
11
|
Cross-reference `progress.md` / `tickets.md` for per-ticket detail.
|
|
12
12
|
|
|
13
|
+
## [0.11.0] - 2026-08-18
|
|
14
|
+
|
|
15
|
+
This is the **security-audit remediation release**. The 2026-08-11
|
|
16
|
+
static audit (`SECURITY-REPORT.md`, 43 findings) is now fully
|
|
17
|
+
remediated: the Critical + first High findings (APIGW body logging,
|
|
18
|
+
OIDC-only deploys) and the CSPRNG id fix shipped in v0.10.0; the
|
|
19
|
+
remaining **40 findings (TICKET-182 … TICKET-222)** land here as a
|
|
20
|
+
layered hardening pass over every edge: **admission control and
|
|
21
|
+
rate limiting at every transport boundary** (global cap, per-IP
|
|
22
|
+
caps, connect-rate windows, per-connection frame budgets, bounded
|
|
23
|
+
line buffers), **credential hardening** (scrypt oper credentials,
|
|
24
|
+
constant-time / timing-equalized comparisons, brute-force lockouts
|
|
25
|
+
on every auth surface), **SASL `EXTERNAL` rebuilt behind an
|
|
26
|
+
operator opt-in with DER-fingerprint account binding**, and a sweep
|
|
27
|
+
of **protocol budget correctness fixes** (UTF-8 byte budgets, tag
|
|
28
|
+
caps, bare-CR rejection).
|
|
29
|
+
|
|
30
|
+
### Added — Admission control & rate limiting (`irc-core`, `cf-adapter`, `aws-adapter`, `aws-stack`)
|
|
31
|
+
|
|
32
|
+
Layered throttling of connection establishment and frame intake,
|
|
33
|
+
enforced at every transport edge (WS upgrade, NLB stream, TCP
|
|
34
|
+
origin) with `429` rejections.
|
|
35
|
+
|
|
36
|
+
- **Global `MAX_CLIENTS` cap with live-connection counter.**
|
|
37
|
+
Cloudflare reserves a slot in a `CounterDO` counter before
|
|
38
|
+
forwarding an upgrade and answers `429` (`ERROR :Closing link:
|
|
39
|
+
server full`) once the cap is reached; slots are released on
|
|
40
|
+
close and TTL-reaped if a DO dies without one. AWS keeps the
|
|
41
|
+
O(1) atomic counter from v0.8.0, now enforced against the
|
|
42
|
+
configured cap.
|
|
43
|
+
- **Per-IP connection caps + sliding-window connect-rate limits.**
|
|
44
|
+
`MAX_CONNECTIONS_PER_IP` (default 10) caps simultaneous
|
|
45
|
+
connections per source IP; `PER_IP_CONNECTION_RATE_MAX` /
|
|
46
|
+
`PER_IP_CONNECTION_RATE_WINDOW_MS` (default 5 / 60 s) cap new
|
|
47
|
+
establishments per window. Cloudflare enforces both at the worker
|
|
48
|
+
edge via a new `RateLimitDO` (sliding-window admission timestamps
|
|
49
|
+
keyed on `CF-Connecting-IP`; rejections never consume budget, so
|
|
50
|
+
a blocked IP recovers after window decay). AWS `$connect` counts
|
|
51
|
+
live + in-window rows via a new `sourceIp+connectedSince` GSI on
|
|
52
|
+
`Connections`, the NLB path applies the same budget to new flows
|
|
53
|
+
(first chunk only, so active sessions are not starved), API
|
|
54
|
+
Gateway stage throttling (rate + burst parameters) backstops
|
|
55
|
+
globally, and an **opt-in WAFv2 per-IP rate rule**
|
|
56
|
+
(`-c wafConnectRateLimit=<n>`) sits at the edge.
|
|
57
|
+
- **Per-connection inbound frame-rate window.**
|
|
58
|
+
`MAX_FRAMES_PER_WINDOW` / `FRAME_WINDOW_SECONDS` (default 50 /
|
|
59
|
+
5 s) enforced at the adapter boundary **before** the actor or any
|
|
60
|
+
storage write — a cheap early rejection for frame floods.
|
|
61
|
+
- **Bounded line-buffer memory.** TCP input buffers are capped at
|
|
62
|
+
**8 KiB** at every transport edge (local CLI, container origin,
|
|
63
|
+
NLB handler, forwarder), and the NLB handler rejects oversized
|
|
64
|
+
payloads outright — an unauthenticated peer can no longer pin
|
|
65
|
+
memory with an unterminated byte stream.
|
|
66
|
+
|
|
67
|
+
### Added — Cloudflare TCP origin hardening (`cf-tcp-container`)
|
|
68
|
+
|
|
69
|
+
The Spectrum origin is rebuilt as a hardened TLS terminator.
|
|
70
|
+
|
|
71
|
+
- **TLS terminates at the origin.** Spectrum switches to
|
|
72
|
+
`tls_mode = "full"` (encrypted all the way to the container, no
|
|
73
|
+
plaintext hop) and the container itself terminates TLS, pinned to
|
|
74
|
+
**TLS 1.2/1.3**.
|
|
75
|
+
- **PROXY protocol v1 is required on every flow.** The container
|
|
76
|
+
parses the `PROXY TCP4/TCP6 …` header (incremental, 107-byte
|
|
77
|
+
budget) to recover the real client IP — and **rejects flows
|
|
78
|
+
without one** — so per-IP admission (above) cannot be spoofed by
|
|
79
|
+
connecting directly to the origin. Origin ingress is locked to
|
|
80
|
+
Spectrum's published IP ranges in the Terraform.
|
|
81
|
+
- **Non-root runtime.** The container runs as the non-root `node`
|
|
82
|
+
user (UID 1000); the Dockerfile drops to an unprivileged user and
|
|
83
|
+
the cert files are owned by it.
|
|
84
|
+
|
|
85
|
+
### Added — AWS stack hardening (`aws-stack`, `aws-adapter`)
|
|
86
|
+
|
|
87
|
+
- **State tables protected.** Every DynamoDB state table
|
|
88
|
+
(`Connections`, `Nicks`, `Channels`, `Services`, …) now carries
|
|
89
|
+
`RETAIN` deletion policy, **deletion protection**, and
|
|
90
|
+
**point-in-time recovery (PITR)** — a stray `cdk destroy` or a
|
|
91
|
+
compromised console session can no longer silently destroy
|
|
92
|
+
persistent IRC state.
|
|
93
|
+
- **NLB stream path integrity.** The streaming handler asserts the
|
|
94
|
+
flow is TLS-secured (surfacing user mode `S`), **rejects flows
|
|
95
|
+
with missing or malformed `x-forwarded-for` / `x-forwarded-port`
|
|
96
|
+
headers**, and its Lambda now holds the
|
|
97
|
+
`execute-api:ManageConnections` grant against the management API
|
|
98
|
+
stage (fixes silent fanout failure on the TCP path).
|
|
99
|
+
- **APIGW logging role least-privilege.** The CloudWatch logging
|
|
100
|
+
role's policy is scoped to exact write-only grants on the
|
|
101
|
+
API-specific log groups.
|
|
102
|
+
|
|
103
|
+
### Added — Credential hardening (`irc-core`)
|
|
104
|
+
|
|
105
|
+
- **Scrypt-hashed oper credentials.** `OPER` verifies against a
|
|
106
|
+
scrypt hash + salt (`OPER_SALT` + `OPER_HASH`) with a timing-safe
|
|
107
|
+
compare; a new `tools/hash-oper-cred.ts` generates the pair
|
|
108
|
+
(`node --import tsx tools/hash-oper-cred.ts --user admin --stdin`).
|
|
109
|
+
The plaintext `OPER_PASSWORD` form still works but logs a
|
|
110
|
+
deprecation warning and will be removed.
|
|
111
|
+
- **Constant-time server-password comparison.** The
|
|
112
|
+
`SERVER_PASSWORD` gate compares in constant time.
|
|
113
|
+
- **Timing-equalized nick verification (no account enumeration).**
|
|
114
|
+
`verifyNick` runs a dummy scrypt verify for unknown accounts, so
|
|
115
|
+
response timing cannot distinguish "no such nick" from "wrong
|
|
116
|
+
password". NickServ `INFO` / `SET PASSWORD` replies are already
|
|
117
|
+
indistinguishable on the same axis.
|
|
118
|
+
- **Brute-force lockouts on every auth surface.** A per-connection
|
|
119
|
+
**SASL failure lockout** (repeated `904`s disconnect with "too
|
|
120
|
+
many failed SASL attempts"), a **per-IP `OPER` failure lockout**
|
|
121
|
+
(with oper-broadcast on trip), and a **per-account NickServ
|
|
122
|
+
`IDENTIFY` freeze**. The `AUTHENTICATE` **flood-control exemption
|
|
123
|
+
is removed** — SASL frames now count against the connection's
|
|
124
|
+
message bucket, closing the timing-oracle bandwidth amplifier.
|
|
125
|
+
- **Credential redaction from parse-error logs.** Parse-error log
|
|
126
|
+
records carry only a truncated leading token, the length, and a
|
|
127
|
+
stable reason code — never the raw line (a malformed line could
|
|
128
|
+
otherwise smuggle `PASS <secret>` past the parser into logs).
|
|
129
|
+
Workers log sampling is lowered to **10%** by default.
|
|
130
|
+
|
|
131
|
+
### Changed — SASL `EXTERNAL` (mTLS) rebuilt behind an operator opt-in (`irc-core`, adapters)
|
|
132
|
+
|
|
133
|
+
EXTERNAL is now advertised and accepted only when **all three**
|
|
134
|
+
hold: a bound mTLS identity source (CF API Shield / APIGW client
|
|
135
|
+
certs), the operator opt-in **`EXTERNAL_ENABLED`** (`true`/`1`,
|
|
136
|
+
**default off** — surfaced via both adapters' config loaders), and
|
|
137
|
+
a secure (TLS) connection. The `sasl` cap value and the
|
|
138
|
+
`AUTHENTICATE` reducer share the same gate so advertisement and
|
|
139
|
+
enforcement cannot drift; refusals answer `908 ERR_SASLMECHS`
|
|
140
|
+
listing `PLAIN` only and do not count toward the SASL lockout.
|
|
141
|
+
|
|
142
|
+
- **Accounts bind to the certificate's DER SHA-256 fingerprint**
|
|
143
|
+
(`fp:<hex>` entries in `RegisteredNick.certSubjects` — what
|
|
144
|
+
Cloudflare surfaces from `request.cf.tlsClientAuth`; the preferred
|
|
145
|
+
binding) **or a canonical subject DN** (the only identifier API
|
|
146
|
+
Gateway exposes). DNs are canonicalised before comparison —
|
|
147
|
+
attribute types lower-cased, whitespace collapsed, RDN/AVA
|
|
148
|
+
components sorted — so semantically equal DNs written differently
|
|
149
|
+
(including across cert re-issuance) still match. Legacy verbatim
|
|
150
|
+
bindings verify and are rewritten to the canonical spelling on
|
|
151
|
+
first use.
|
|
152
|
+
- **Transports without a client-cert surface pin the mechanism off.**
|
|
153
|
+
The `cf-tcp-container` Spectrum origin (and the NLB stream handler
|
|
154
|
+
unless an mTLS provider is injected) reject
|
|
155
|
+
`AUTHENTICATE EXTERNAL` outright with a transport-specific
|
|
156
|
+
`904 ERR_SASLFAIL` instead of silently mis-binding.
|
|
157
|
+
|
|
158
|
+
### Changed — Protocol budget correctness (`irc-core`, `irc-server`)
|
|
159
|
+
|
|
160
|
+
- **All length budgets are enforced in UTF-8 bytes, not UTF-16 code
|
|
161
|
+
units** — the 512-byte line cap, `TOPICLEN` (truncated on
|
|
162
|
+
character boundaries, never mid-multibyte), `NICKLEN` /
|
|
163
|
+
`CHANNELLEN`, and the `draft/multiline` batch budget, which is
|
|
164
|
+
now also **enforced incrementally as lines arrive** (an
|
|
165
|
+
over-budget batch aborts mid-stream with `FAIL`, not just at
|
|
166
|
+
close).
|
|
167
|
+
- **IRCv3 tag limits enforced in the parser**: at most **15 tags**
|
|
168
|
+
and an **8191-byte tag section** per message.
|
|
169
|
+
- **Multi-target commands capped.** Comma-split
|
|
170
|
+
`PRIVMSG`/`NOTICE`/`TAGMSG`/`PART` targets beyond
|
|
171
|
+
`MAX_TARGETS_PER_COMMAND` are dropped with `407 ERR_TOOMANYTARGETS`.
|
|
172
|
+
- **Long `353 NAMES` replies split** across multiple lines within
|
|
173
|
+
the 510-byte budget (previously one over-long line).
|
|
174
|
+
- **`CHATHISTORY` limits capped** at a configurable ceiling
|
|
175
|
+
(default 100) — `CHATHISTORY LATEST #chan * 999999999` no longer
|
|
176
|
+
asks for an unbounded replay.
|
|
177
|
+
- **Bare-CR smuggling rejected.** Any CR remaining after the
|
|
178
|
+
trailing CRLF strip is a parse error (`421`), and outbound
|
|
179
|
+
`RawLine.text` is sanitized of stray CR.
|
|
180
|
+
- **Channel names validated in one shared module** — NUL and
|
|
181
|
+
control characters are forbidden outright.
|
|
182
|
+
- **`USER` username/realname validated** (charset, byte length, no
|
|
183
|
+
control characters) before `001`.
|
|
184
|
+
- **`OPER` requires a registered connection** (`451
|
|
185
|
+
ERR_NOTREGISTERED` pre-registration).
|
|
186
|
+
- **`parseTags` builds a null-prototype map** — a `__proto__` tag
|
|
187
|
+
can no longer pollute the tags object (prototype pollution).
|
|
188
|
+
|
|
189
|
+
### Changed — Services hardening (`irc-core`)
|
|
190
|
+
|
|
191
|
+
- **ChanServ privilege guards.** `DEOP`/`KICK` cannot target the
|
|
192
|
+
founder from a non-founder, and roster-mutation commands check the
|
|
193
|
+
caller's access rank (`SOP`/`AOP`/`HOP`/`VOP`) before acting.
|
|
194
|
+
- **HostServ auto-approve denylist/allowlist.** When vhost
|
|
195
|
+
auto-approve is enabled, requests are checked against a
|
|
196
|
+
configurable denylist (and optional allowlist) instead of
|
|
197
|
+
approving everything.
|
|
198
|
+
|
|
199
|
+
### Added — CI / deploy guards (`ci-hardening`, deploy workflows)
|
|
200
|
+
|
|
201
|
+
- **Env-var documentation drift guard.** The
|
|
202
|
+
`consumed-env-vars` block in `apps/cf-worker/wrangler.toml` is
|
|
203
|
+
contract-checked against the cf-adapter config loader source on
|
|
204
|
+
every CI run: every consumed var must be documented (classified
|
|
205
|
+
`[vars]` vs `[secret]`), no stale names, and no `[secret]`-class
|
|
206
|
+
var may ship as a plaintext `[vars]` entry.
|
|
207
|
+
- **Placeholder-hostname deploy rejection.** The CF deploy workflow
|
|
208
|
+
refuses to ship while `SERVER_NAME` is still a repo placeholder
|
|
209
|
+
(`irc.example.com` / `irc.your-domain.invalid`).
|
|
210
|
+
|
|
211
|
+
### Changed — Documentation
|
|
212
|
+
|
|
213
|
+
- **`README.md` rewritten to current capabilities**: the abuse
|
|
214
|
+
controls & credential hardening surface (admission/rate table,
|
|
215
|
+
protocol budgets, lockouts, the SASL EXTERNAL triple gate), the
|
|
216
|
+
hardened TCP origins (PROXY v1 + origin TLS on CF, NLB assertions
|
|
217
|
+
on AWS), the new tools, accurate coverage state, and an updated
|
|
218
|
+
roadmap.
|
|
219
|
+
|
|
220
|
+
### ⚠️ Migration required
|
|
221
|
+
|
|
222
|
+
- **No persisted-DO-state schema change.** `PERSISTED_STATE_VERSION`
|
|
223
|
+
stays `1`; the D1/DynamoDB services tables are unchanged (the
|
|
224
|
+
canonical-DN rewrite happens lazily at compare time and is
|
|
225
|
+
backward-compatible). **Code rollback across this release is
|
|
226
|
+
safe.**
|
|
227
|
+
- **SASL `EXTERNAL` is now OFF by default.** If you rely on mTLS
|
|
228
|
+
EXTERNAL, set `EXTERNAL_ENABLED=true` (Workers `[vars]` / Lambda
|
|
229
|
+
env) **and** ensure edge mTLS (API Shield / APIGW client certs)
|
|
230
|
+
is configured — the flag alone advertises nothing. Accounts
|
|
231
|
+
should re-bind to the certificate's DER fingerprint
|
|
232
|
+
(`fp:<hex>`) where the platform surfaces it.
|
|
233
|
+
- **CF TCP origin: reconfigure Spectrum before deploying.** The
|
|
234
|
+
container now terminates TLS and **requires PROXY protocol v1**
|
|
235
|
+
on every flow. Update the Spectrum app to
|
|
236
|
+
`proxy_protocol = "v1"` + `tls_mode = "full"`, mount the origin
|
|
237
|
+
certificate readable by UID 1000, and keep origin ingress locked
|
|
238
|
+
to Spectrum's IP ranges. See `docs/Cloudflare-TCP-Deployment.md`.
|
|
239
|
+
- **AWS: NLB target must preserve flow headers.** The stream
|
|
240
|
+
handler now rejects flows missing `x-forwarded-for` /
|
|
241
|
+
`x-forwarded-port` (an NLB target-group attribute) — a
|
|
242
|
+
misconfigured LB now fails loudly instead of mis-attributing IPs.
|
|
243
|
+
- **Admission defaults are now ON.** `MAX_CONNECTIONS_PER_IP=10`,
|
|
244
|
+
per-user 5, connect-rate 5/60 s, and 50 frames / 5 s apply even
|
|
245
|
+
when unset. Deployments behind a shared NAT / proxy (offices,
|
|
246
|
+
bridges, health probes) should raise `MAX_CONNECTIONS_PER_IP` /
|
|
247
|
+
the rate budget to taste.
|
|
248
|
+
- **Oper credentials: migrate off plaintext.** `OPER_PASSWORD`
|
|
249
|
+
still works but is deprecated. Generate
|
|
250
|
+
`OPER_SALT`/`OPER_HASH` with `tools/hash-oper-cred.ts` and set
|
|
251
|
+
them as Workers secrets / Lambda env.
|
|
252
|
+
- **Client-visible behaviour changes.** (1) `AUTHENTICATE` frames
|
|
253
|
+
now count against flood control (a client that sent SASL at line
|
|
254
|
+
rate will be disconnected as an excess flood). (2) An inbound
|
|
255
|
+
frame with an embedded bare CR is now a parse error answered with
|
|
256
|
+
`421` (previously in legacy mode it could smuggle content).
|
|
257
|
+
(3) Multi-target commands beyond the cap get `407` for the
|
|
258
|
+
dropped targets. No compliant client is known to trip any of
|
|
259
|
+
these.
|
|
260
|
+
|
|
261
|
+
### Known limitations
|
|
262
|
+
|
|
263
|
+
- **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
|
|
264
|
+
TheLounge / matrix-IRC bridge) remains pending.
|
|
265
|
+
- **Web client e2e** (Playwright headless-browser vs a deployed
|
|
266
|
+
stack) remains pending; the AWS staging frontend e2e
|
|
267
|
+
(TICKET-169) is still open.
|
|
268
|
+
- Same transport limits as v0.10.0: the **CF TCP path requires
|
|
269
|
+
Cloudflare Spectrum (Enterprise tier)**; the **AWS TCP path** is
|
|
270
|
+
subject to Lambda stream-duration limits. WebSocket remains the
|
|
271
|
+
zero-extra-deps default.
|
|
272
|
+
- **Coverage hardening is near-complete.** `irc-core`,
|
|
273
|
+
`irc-server`, `in-memory-runtime`, `irc-test-support`,
|
|
274
|
+
`cf-worker`, `local-cli`, `aws-stack`, `load-test`,
|
|
275
|
+
`cf-tcp-container`, `tcp-ws-forwarder`, `ci-hardening`, and
|
|
276
|
+
`deploy-scripts` sit at 100% lines; `cf-adapter` (~99.7%) and
|
|
277
|
+
`aws-adapter` (~98%) clear the 90% gate with follow-ups open;
|
|
278
|
+
`web` is at 100% lines / ~98% branches.
|
|
279
|
+
|
|
280
|
+
### Security
|
|
281
|
+
|
|
282
|
+
This release is itself a security remediation; the headline items:
|
|
283
|
+
|
|
284
|
+
- **Admission + rate limiting at every edge** (above) — the audit's
|
|
285
|
+
"no throttling anywhere" theme: connection floods, per-IP
|
|
286
|
+
takeovers of the connection budget, and frame floods are now
|
|
287
|
+
bounded by default.
|
|
288
|
+
- **Plaintext oper credentials replaced** with scrypt hashes +
|
|
289
|
+
timing-safe verify (above).
|
|
290
|
+
- **Account-enumeration oracles closed**: timing-equalized
|
|
291
|
+
`verifyNick`, constant-time server-password compare (above).
|
|
292
|
+
- **Brute-force lockouts** on SASL / OPER / NickServ `IDENTIFY`
|
|
293
|
+
(above).
|
|
294
|
+
- **SASL `EXTERNAL` safe by default** (off unless the operator
|
|
295
|
+
opts in; fingerprint-based binding) and **bare-CR / prototype-
|
|
296
|
+
pollution / oversized-payload parser hardening** (above).
|
|
297
|
+
- **Credential redaction from logs** + 10% default sampling
|
|
298
|
+
(above), complementing v0.10.0's `DataTraceEnabled` hard-lock.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## [0.10.0] - 2026-08-13
|
|
303
|
+
|
|
304
|
+
The main change in this release is **AWS web client parity** — the
|
|
305
|
+
vendored Kiwi IRC SPA is no longer Cloudflare-only. AWS now hosts it
|
|
306
|
+
via an **S3 + CloudFront + OAC** `StaticSite` construct (opt-in via
|
|
307
|
+
CDK context) with a **stack-output-driven two-phase deploy**, and the
|
|
308
|
+
API Gateway `$connect` route gains an opt-in `WEB_ORIGINS` CSWSH
|
|
309
|
+
defence that mirrors the CF Worker's. Alongside it: the SASL
|
|
310
|
+
`AccountStore` is **collapsed into `ServicesStore`** as the single
|
|
311
|
+
credential home (a one-shot backfill migration ships for existing
|
|
312
|
+
deployments), **NickServ `SET PASSWORD`** lands as self-service
|
|
313
|
+
password change, and the staging/prod deploy split is **collapsed**
|
|
314
|
+
into a single target per platform. A round of **AWS deploy security
|
|
315
|
+
hardening** ships: the CI workflow is **OIDC-only** (no long-lived
|
|
316
|
+
access keys), and API Gateway full-frame body logging
|
|
317
|
+
(`DataTraceEnabled`) is **off by default and hard-locked** so IRC
|
|
318
|
+
frames are never written to CloudWatch. Finally, `UuidIdFactory` now
|
|
319
|
+
sources its random bytes from `globalThis.crypto` (CSPRNG) instead of
|
|
320
|
+
`Math.random`, so generated `msgid` / session ids are cryptographic-
|
|
321
|
+
grade.
|
|
322
|
+
|
|
323
|
+
### Added — AWS web client (`@serverless-ircd/aws-stack`, `@serverless-ircd/aws-adapter`, `@serverless-ircd/web`)
|
|
324
|
+
|
|
325
|
+
The web client is no longer Cloudflare-only. AWS provisions a
|
|
326
|
+
static-site origin for the SPA while the wss IRC endpoint stays on
|
|
327
|
+
API Gateway — two origins, same `apps/web/dist/` artifact.
|
|
328
|
+
|
|
329
|
+
- **`StaticSite` construct** (`apps/aws-stack/src/static-site.ts`): a
|
|
330
|
+
private S3 origin bucket fronted by a CloudFront distribution with
|
|
331
|
+
**Origin Access Control (OAC)** — direct S3 access is denied by the
|
|
332
|
+
bucket policy; only CloudFront URLs resolve. `defaultRootObject` is
|
|
333
|
+
`index.html` (the project landing page at `/`), and a custom error
|
|
334
|
+
response maps S3 `403`/`404` → `200 /webclient/index.html` so
|
|
335
|
+
client-side SPA routes resolve. Custom domain (ACM certificate +
|
|
336
|
+
Route53 `ARecord` alias) is opt-in via CDK context
|
|
337
|
+
(`webSiteCustomDomain` / `webSiteCertificateArn` /
|
|
338
|
+
`webSiteHostedZoneName` / `webSiteHostedZoneId`); omitted by default
|
|
339
|
+
to use `*.cloudfront.net`. Three stable CfnOutputs (`WebsiteURL`,
|
|
340
|
+
`WebsiteBucketName`, `WebsiteDistributionId`) drive the deploy
|
|
341
|
+
pipeline below.
|
|
342
|
+
- **CloudFront directory-index viewer-request function**: S3 REST
|
|
343
|
+
(used with OAC) does not serve index documents, so `/docs/` or
|
|
344
|
+
`/docs` would 404 and fall through to the SPA error response
|
|
345
|
+
(serving the Kiwi shell instead of the rendered docs index). A
|
|
346
|
+
viewer-request CloudFront function now resolves directory-style
|
|
347
|
+
URIs: trailing-slash paths (`/docs/`, `/`) get `index.html`
|
|
348
|
+
appended as an internal rewrite, and extensionless paths without a
|
|
349
|
+
trailing slash (`/docs`) get a `301` redirect to the trailing-slash
|
|
350
|
+
form so the browser resolves relative asset links against the
|
|
351
|
+
directory. Real assets (`.html`, `.css`, `.js`, …) are served
|
|
352
|
+
unchanged. Mirrors the CF Worker `[assets]` binding, which resolves
|
|
353
|
+
directory indexes automatically.
|
|
354
|
+
- **`$connect` Origin allowlist (CSWSH defence)**: API Gateway
|
|
355
|
+
WebSocket has no built-in `Origin` validation, so the `$connect`
|
|
356
|
+
Lambda now enforces it. `WEB_ORIGINS` (comma-separated) is parsed
|
|
357
|
+
into a normalised lowercase set at cold start; a browser-sent
|
|
358
|
+
`Origin` not in the set is denied with `403` (API Gateway closes
|
|
359
|
+
the upgrade). The defence is **opt-in** — unset `WEB_ORIGINS`
|
|
360
|
+
disables the check entirely so existing bare-IRC deployments without
|
|
361
|
+
a web frontend are unchanged on upgrade. Non-browser clients (curl,
|
|
362
|
+
WeeChat, the `tcp-ws-forwarder`, the NLB TCP+TLS path) never send
|
|
363
|
+
`Origin` and always proceed. Deliberate divergence from the CF
|
|
364
|
+
policy: **explicit-allowlist only** (no same-origin auto-derive),
|
|
365
|
+
because the SPA is on a CloudFront origin and the wss endpoint is
|
|
366
|
+
on an API Gateway origin, so the request's own host is never the
|
|
367
|
+
SPA's origin. Mirrors the CF Worker's parsing semantics (trimmed,
|
|
368
|
+
lowercased, scheme+host match).
|
|
369
|
+
- **`prod-aws` build env**: a new `apps/web/static/config.prod-aws.json`
|
|
370
|
+
+ `--env prod-aws` (and a `build:prod-aws` pnpm script) select the
|
|
371
|
+
AWS API-Gateway-shaped config. The `server` field carries
|
|
372
|
+
`{{API_ID}}` / `{{REGION}}` / `{{STAGE}}` placeholders the deploy
|
|
373
|
+
pipeline substitutes at bake time, so no concrete wss URL is
|
|
374
|
+
hardcoded in the repo. Because irc-framework prepends `wss://`
|
|
375
|
+
itself from `tls: true`, the baked URL is **split** into a
|
|
376
|
+
hostname-only `server` and a stage-name `direct_path` (e.g.
|
|
377
|
+
`/<stage>`) — baking the full `wss://` URL into `server` produced
|
|
378
|
+
a doubled-scheme `wss://wss//...` URL that Firefox rejects.
|
|
379
|
+
- **Stack-output-driven deploy pipeline** (`scripts/deploy-web-aws.mjs`,
|
|
380
|
+
a new `@serverless-ircd/deploy-scripts` workspace package): unlike
|
|
381
|
+
the CF Worker (where `wrangler deploy` ships the SPA in the same
|
|
382
|
+
command as the runtime), the AWS SPA deploy is **two-phase** because
|
|
383
|
+
the baked `config.json` carries the API Gateway WebSocket URL as a
|
|
384
|
+
literal that does not exist at synth time. The script runs
|
|
385
|
+
`describe-stacks` → `pnpm --filter web build:prod-aws -- --api-url
|
|
386
|
+
<ConnectUrl>` → `aws s3 sync --delete` →
|
|
387
|
+
`cloudfront create-invalidation /*`. Fails loudly on missing stack
|
|
388
|
+
outputs or unsubstituted `{{...}}` placeholders. The
|
|
389
|
+
`deploy-web` CI job (`.github/workflows/deploy-aws.yml`) runs it
|
|
390
|
+
with `needs: deploy`.
|
|
391
|
+
|
|
392
|
+
### Added — NickServ `SET PASSWORD` (`@serverless-ircd/irc-core`)
|
|
393
|
+
|
|
394
|
+
- **Self-service password change.** `PRIVMSG NickServ :SET PASSWORD
|
|
395
|
+
<old> <new>` re-verifies the current password (defence against a
|
|
396
|
+
hijacked `+r` session), enforces a configurable minimum (default 8)
|
|
397
|
+
and a fixed maximum (256, the scrypt input budget), then re-hashes
|
|
398
|
+
and persists via `ServicesStore.setNickPassword`. The caller's
|
|
399
|
+
session stays `+r`; other sessions on the same account are not
|
|
400
|
+
kicked. Wrong old password is indistinguishable from "no such
|
|
401
|
+
account" (no enumeration vector). Wired through
|
|
402
|
+
`InMemoryServicesStore` and `PersistentServicesStore` with round-
|
|
403
|
+
trip tests on the D1 and DynamoDB backends.
|
|
404
|
+
|
|
405
|
+
### Added — CSPRNG-backed ids (`@serverless-ircd/irc-core`)
|
|
406
|
+
|
|
407
|
+
- **`UuidIdFactory` now uses `globalThis.crypto.getRandomValues`.** The
|
|
408
|
+
previous `Math.random`-based UUIDv4 generation was unsuitable for
|
|
409
|
+
unforgeable `msgid` / session ids — a predictable id would let a
|
|
410
|
+
client forge message attribution. The global `crypto.getRandomValues`
|
|
411
|
+
is available on Node ≥ 19 and Cloudflare Workers without an import.
|
|
412
|
+
Generated ids are now cryptographic-grade. The `IdFactory` port and
|
|
413
|
+
the deterministic `testIdFactory` test seam are unchanged, so
|
|
414
|
+
reducer determinism (reducers never touch real randomness) is
|
|
415
|
+
preserved; only the production factory changed.
|
|
416
|
+
|
|
417
|
+
### Changed — Single credential home (`@serverless-ircd/irc-core`, `@serverless-ircd/cf-adapter`, `@serverless-ircd/aws-adapter`)
|
|
418
|
+
|
|
419
|
+
The SASL `AccountStore` port is **removed**. `ServicesStore` is now
|
|
420
|
+
the single credential home: SASL PLAIN, SASL EXTERNAL (CertFP),
|
|
421
|
+
NickServ `IDENTIFY`, and `PASS <nick>:<password>` all verify through
|
|
422
|
+
the same scrypt-hashed `verifyNick` / `verifyCertFP` surface. This
|
|
423
|
+
closes the cross-store credential drift flagged as a known limitation
|
|
424
|
+
in v0.9.0 — a nick registered via NickServ `REGISTER` after the worker
|
|
425
|
+
booted now authenticates via SASL/PASS immediately, because there is
|
|
426
|
+
no second store to drift out of sync with.
|
|
427
|
+
|
|
428
|
+
- **`ServicesStore` grows CertFP**: `verifyCertFP` / `addCertFP` /
|
|
429
|
+
`removeCertFP` are added to the port, and `RegisteredNick` gains a
|
|
430
|
+
`certSubjects: string[]` field so SASL EXTERNAL resolves through the
|
|
431
|
+
services store (previously SASL EXTERNAL only worked against the
|
|
432
|
+
static `InMemoryAccountStore`, never the hashed D1/DynamoDB table).
|
|
433
|
+
- **D1 `cert_subjects` column auto-migrates**: the CF D1 path runs
|
|
434
|
+
`ALTER TABLE nickserv_accounts ADD COLUMN cert_subjects` inside
|
|
435
|
+
`migrateServicesSchema` on cold start (`CREATE TABLE IF NOT EXISTS`
|
|
436
|
+
+ the `ALTER`), so no manual DDL is needed. DynamoDB is schemaless.
|
|
437
|
+
- **Removed**: `AccountStore` port, `InMemoryAccountStore`,
|
|
438
|
+
`HashedAccountStore`, the D1/DynamoDB `account-store`
|
|
439
|
+
implementations, `SaslPayload` / `SaslResult` / `SaslAccountCredential`
|
|
440
|
+
/ `constantTimeEquals` / `ingestAccountCredential`, and the
|
|
441
|
+
`accounts` / `Accounts` table reads. The new code only reads
|
|
442
|
+
`nickserv_accounts` / `Services`.
|
|
443
|
+
- **`tools/migrate-accounts-to-services.ts`**: a one-shot, idempotent,
|
|
444
|
+
never-overwrite backfill of legacy `accounts` / `Accounts` rows into
|
|
445
|
+
`nickserv_accounts` / `Services`. Reads the still-deployed
|
|
446
|
+
`SASL_ACCOUNTS` env var at run time so env-seeded accounts are
|
|
447
|
+
carried into the services table. `cert_subjects` defaults to `'[]'`
|
|
448
|
+
for every migrated row (the legacy table had no cert column).
|
|
449
|
+
**Run this before the v0.10.0 build deploys** — see the migration
|
|
450
|
+
section below.
|
|
451
|
+
|
|
452
|
+
### Changed — Single deploy target per platform
|
|
453
|
+
|
|
454
|
+
The staging/prod deploy split is **collapsed** into one target per
|
|
455
|
+
platform. Staging vs production isolation is now driven by which
|
|
456
|
+
account/region (AWS) or which Worker credentials (CF) the deploy
|
|
457
|
+
targets, not by stack/table/Worker name templating.
|
|
458
|
+
|
|
459
|
+
- **AWS CDK**: drops the `environmentName` prop, `prefixedTableName`,
|
|
460
|
+
and the `IrcAwsStack-<env>` stack id. Tables use bare logical ids
|
|
461
|
+
(`Connections`, `Nicks`, …); the stack is always `IrcAwsStack`.
|
|
462
|
+
- **Cloudflare**: drops the `env.staging` blocks in the cf-worker and
|
|
463
|
+
cf-tcp-container wrangler configs.
|
|
464
|
+
- **Web build**: drops `build:staging` / `config.staging.json`; `--env`
|
|
465
|
+
now accepts only `prod` (or the default dev config) and `prod-aws`.
|
|
466
|
+
- **CI**: collapses `deploy-*: staging` jobs into single `deploy` /
|
|
467
|
+
`deploy-web` jobs; script names align (`pnpm deploy:cf`,
|
|
468
|
+
`pnpm deploy:aws`, `pnpm deploy:cf:tcp`, `pnpm smoke:aws`,
|
|
469
|
+
`pnpm smoke:cf`). Deploy workflows remain `workflow_dispatch`-only
|
|
470
|
+
(no push trigger).
|
|
471
|
+
|
|
472
|
+
### Added — AWS deploy security
|
|
473
|
+
|
|
474
|
+
- **OIDC-only CI deploys.** The GitHub Actions deploy workflow
|
|
475
|
+
(`deploy-aws.yml`) accepts **only** GitHub OIDC web-identity
|
|
476
|
+
credentials. `aws-access-key-id` / `aws-secret-access-key` inputs
|
|
477
|
+
are dropped from every `configure-aws-credentials` step in both the
|
|
478
|
+
`deploy` and `deploy-web` jobs; the workflow requires
|
|
479
|
+
`AWS_DEPLOY_ROLE_ARN` and fails fast with `::error::` if it is
|
|
480
|
+
unset, then asserts `aws sts get-caller-identity` matches the
|
|
481
|
+
configured role before any `cdk deploy` (defence-in-depth against a
|
|
482
|
+
silent fallback to ambient static credentials). Long-lived access
|
|
483
|
+
keys powerful enough to deploy CloudFormation + IAM + Lambda +
|
|
484
|
+
DynamoDB are effectively account-admin; a single exfil vector
|
|
485
|
+
yields full account takeover. The full IAM trust policy snippet,
|
|
486
|
+
condition keys, and verification steps are in
|
|
487
|
+
`docs/AWS-Deployment.md` §17. A `ci-hardening` lint + test suite
|
|
488
|
+
(`findAwsCredentialSteps`, `readWorkflowPermissions`) locks the
|
|
489
|
+
OIDC-only contract on every PR.
|
|
490
|
+
- **API Gateway `DataTraceEnabled` off by default and hard-locked.**
|
|
491
|
+
Full-frame body tracing wrote every IRC frame (`PASS <password>`,
|
|
492
|
+
`AUTHENTICATE <base64-SASL-PLAIN>`, `JOIN #chan <key>`,
|
|
493
|
+
`PRIVMSG`/`NOTICE`) to the APIGW execution-log group, leaking
|
|
494
|
+
credentials to anyone with `logs:GetLogEvents`. Now safe-by-default
|
|
495
|
+
(`false`); because the staging/prod split collapsed, there is no
|
|
496
|
+
quiet "non-prod" path to re-enable it. The only way back on is an
|
|
497
|
+
explicit **two-step opt-in** (`-c allowDataTrace=true` **and** `-c
|
|
498
|
+
iUnderstandThisLeaksCredentials=true`) that refuses synthesis unless
|
|
499
|
+
both flags are set — see `docs/AWS-Deployment.md` §7.6. Tear the
|
|
500
|
+
sandbox stack down immediately after debugging; never deploy that
|
|
501
|
+
combination to a shared account.
|
|
502
|
+
|
|
503
|
+
### Changed — Documentation
|
|
504
|
+
|
|
505
|
+
- **`README.md` updated** to cover AWS web client hosting (the web
|
|
506
|
+
client section was previously Cloudflare-only), the AWS `$connect`
|
|
507
|
+
CSWSH defence, the OIDC-only deploy model, the `DataTraceEnabled`
|
|
508
|
+
hard-lock, and the `scripts/deploy-web-aws.mjs` two-phase deploy.
|
|
509
|
+
The CF-vs-AWS origin difference is laid out in a table, and the
|
|
510
|
+
CSWSH section now documents both adapters' modes (same-origin
|
|
511
|
+
auto-derive on CF; explicit-allowlist-only on AWS).
|
|
512
|
+
|
|
513
|
+
### ⚠️ Migration required
|
|
514
|
+
|
|
515
|
+
- **Run `tools/migrate-accounts-to-services.ts` BEFORE deploying
|
|
516
|
+
v0.10.0** if the deployment has any rows in the legacy `accounts`
|
|
517
|
+
(CF D1) / `Accounts` (AWS DynamoDB) table. The v0.10.0 build only
|
|
518
|
+
reads `nickserv_accounts` / `Services`, so legacy rows that are not
|
|
519
|
+
backfilled will stop authenticating. The script is one-shot,
|
|
520
|
+
idempotent (`INSERT OR IGNORE` on D1 / conditional `PutItem` on
|
|
521
|
+
DynamoDB), and never-overwrites a nick already registered in the
|
|
522
|
+
services table. It reads the still-deployed `SASL_ACCOUNTS` env var
|
|
523
|
+
at run time so env-seeded accounts are carried through. See the
|
|
524
|
+
script header for the per-platform invocation:
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
# Cloudflare D1
|
|
528
|
+
node --import tsx tools/migrate-accounts-to-services.ts \
|
|
529
|
+
--platform cf --database <d1-name> --remote
|
|
530
|
+
|
|
531
|
+
# AWS DynamoDB
|
|
532
|
+
node --import tsx tools/migrate-accounts-to-services.ts \
|
|
533
|
+
--platform aws --accounts-table Accounts --services-table Services \
|
|
534
|
+
--region us-east-1
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
- **Single deploy target: rename your deploy commands.** The
|
|
538
|
+
staging-specific commands are gone. Update any scripts, runbooks, or
|
|
539
|
+
CI that called `pnpm deploy:cf:staging` / `pnpm deploy:aws:staging`
|
|
540
|
+
/ `--env staging` to the single-target form (`pnpm deploy:cf`,
|
|
541
|
+
`pnpm deploy:aws`). Staging vs production isolation is now by
|
|
542
|
+
account/region (AWS) or by Worker credentials (CF), not by command
|
|
543
|
+
suffix.
|
|
544
|
+
- **AWS CI: switch to OIDC.** If the repo still has
|
|
545
|
+
`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` secrets, delete them
|
|
546
|
+
and invalidate the underlying IAM access key. Configure
|
|
547
|
+
`AWS_DEPLOY_ROLE_ARN` (a GitHub-OIDC-trusted role) per
|
|
548
|
+
`docs/AWS-Deployment.md` §17; there is no static-key fallback.
|
|
549
|
+
- **No persisted-DO-state schema change.** `PERSISTED_STATE_VERSION`
|
|
550
|
+
stays `1`. The `cert_subjects` D1 column is added by an idempotent
|
|
551
|
+
`ALTER TABLE` on cold start; the D1/DynamoDB services-row shape
|
|
552
|
+
change is backward-compatible for reads. **Code rollback across
|
|
553
|
+
this release is safe** provided the account-migration backfill was
|
|
554
|
+
run (rolling back the code re-introduces the legacy `accounts`/
|
|
555
|
+
`Accounts` reads, which still work as long as the table still
|
|
556
|
+
exists — it is not auto-dropped).
|
|
557
|
+
|
|
558
|
+
### Known limitations
|
|
559
|
+
|
|
560
|
+
- **The web client remains partial.** The Playwright headless-browser
|
|
561
|
+
e2e against a deployed frontend is still pending; the SPA is
|
|
562
|
+
exercised via the WS smoke (`scripts/smoke.mjs`) and the `apps/web`
|
|
563
|
+
unit / build-smoke suite.
|
|
564
|
+
- **The AWS web client `StaticSite` requires a custom domain to
|
|
565
|
+
provision via `bin/aws.ts`.** Provisioning the construct without a
|
|
566
|
+
custom domain (default `*.cloudfront.net` only) currently requires
|
|
567
|
+
editing `bin/aws.ts` to pass `webSite: {}` directly. Tracked as a
|
|
568
|
+
follow-up.
|
|
569
|
+
- Same transport limits as v0.9.0: the **CF TCP path requires
|
|
570
|
+
Cloudflare Spectrum (Enterprise tier)** plus a stateful Container
|
|
571
|
+
origin; the **AWS TCP path** uses NLB + Lambda streaming and is
|
|
572
|
+
subject to Lambda idle-timeout / stream-duration limits. The
|
|
573
|
+
WebSocket path remains the zero-extra-deps default.
|
|
574
|
+
- **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
|
|
575
|
+
CA/trust-store** on both platforms and is not enabled by default.
|
|
576
|
+
The `cert_subjects` plumbing lands the server-side storage; the
|
|
577
|
+
adapter-side client-cert capture at the TLS edge remains a
|
|
578
|
+
follow-up.
|
|
579
|
+
- **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
|
|
580
|
+
TheLounge / matrix-IRC bridge) remains pending.
|
|
581
|
+
- **Coverage hardening is partial.** `irc-core`, `irc-server`,
|
|
582
|
+
`in-memory-runtime`, and **`cf-adapter`** (new this release) sit at
|
|
583
|
+
100%; `aws-adapter` and `aws-stack` clear the 90% gate. The
|
|
584
|
+
remaining packages (`local-cli`, `load-test`, `cf-tcp-container`,
|
|
585
|
+
`tcp-ws-forwarder`, `irc-test-support`, `web`) are **above the gate
|
|
586
|
+
but below 100%** — follow-ups drive each to full coverage.
|
|
587
|
+
|
|
588
|
+
### Security
|
|
589
|
+
|
|
590
|
+
- **OIDC-only AWS deploys** (above): removes long-lived access keys
|
|
591
|
+
from the deploy path — the highest-impact exfil vector against an
|
|
592
|
+
AWS account with deploy permissions.
|
|
593
|
+
- **APIGW `DataTraceEnabled` hard-locked off** (above): IRC frames
|
|
594
|
+
(including `PASS`, `AUTHENTICATE <SASL-PLAIN>`, channel keys) are
|
|
595
|
+
no longer written to CloudWatch even if an operator toggles the
|
|
596
|
+
console setting by hand; the two-flag escape hatch cannot be set by
|
|
597
|
+
accident.
|
|
598
|
+
- **CSPRNG-backed `msgid` / session ids** (above): generated ids are
|
|
599
|
+
now cryptographic-grade, closing a forgery vector in the previous
|
|
600
|
+
`Math.random`-based `UuidIdFactory`.
|
|
601
|
+
- No other auth path changed. The scrypt-hashed credential store,
|
|
602
|
+
server-password gate, and CF-side CSWSH defence are unchanged from
|
|
603
|
+
v0.9.0.
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
13
607
|
## [0.9.0] - 2026-08-10
|
|
14
608
|
|
|
15
609
|
The main change in this release is **rounding out the integrated IRC
|