serverless-ircd 0.10.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-cf-tcp.yml +26 -2
- package/.github/workflows/deploy-cf.yml +26 -0
- package/CHANGELOG.md +289 -0
- package/README.md +153 -20
- package/apps/aws-stack/bin/aws.ts +36 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +221 -15
- package/apps/aws-stack/tests/stack.test.ts +450 -16
- package/apps/cf-tcp-container/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -2
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +256 -79
- 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 +17 -4
- package/apps/cf-worker/package.json +2 -2
- 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 +86 -9
- 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 +23 -2
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- package/apps/web/package.json +1 -1
- package/docs/AWS-Deployment.md +123 -22
- package/docs/AWS-TCP-Deployment.md +37 -2
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +9 -2
- package/docs/Cloudflare-TCP-Deployment.md +135 -52
- package/docs/SASL-EXTERNAL.md +175 -0
- package/package.json +3 -3
- 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 +34 -6
- package/packages/aws-adapter/src/config-loader.ts +134 -6
- package/packages/aws-adapter/src/dynamo-services-store.ts +12 -0
- package/packages/aws-adapter/src/handlers/connect.ts +47 -1
- package/packages/aws-adapter/src/handlers/default.ts +95 -6
- package/packages/aws-adapter/src/handlers/index.ts +31 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +132 -8
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/serialize.ts +8 -0
- package/packages/aws-adapter/src/tables.ts +9 -0
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +151 -0
- package/packages/aws-adapter/tests/connect.test.ts +199 -2
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +10 -3
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +123 -1
- package/packages/aws-adapter/tests/handlers.test.ts +57 -1
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +48 -9
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +154 -21
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +47 -5
- package/packages/cf-adapter/src/env.ts +88 -0
- package/packages/cf-adapter/src/index.ts +17 -1
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +104 -15
- 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-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +74 -5
- 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 +192 -1
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -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/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- 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/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 +40 -1
- 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 +146 -2
- package/packages/irc-core/src/commands/sasl.ts +136 -19
- 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 +393 -20
- 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 +8 -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 +395 -36
- 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 +228 -13
- package/packages/irc-core/src/ws-framing.ts +5 -4
- 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/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +182 -2
- package/packages/irc-core/tests/commands/oper.test.ts +560 -2
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +463 -1
- package/packages/irc-core/tests/commands/sasl.test.ts +596 -7
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +2 -0
- package/packages/irc-core/tests/config.test.ts +534 -2
- 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/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 +141 -0
- package/packages/irc-core/tests/ports.test.ts +99 -7
- package/packages/irc-core/tests/services-store.test.ts +376 -14
- package/packages/irc-core/tests/ws-framing.test.ts +45 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +123 -8
- 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 +544 -7
- 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/scripts/package.json +1 -1
- 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 +17 -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-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -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/tcp-ws-forwarder/package.json +1 -1
|
@@ -275,6 +275,35 @@ describe('local-cli TCP transport — IRC flows', () => {
|
|
|
275
275
|
await client.close();
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
+
it('closes the TCP connection with ERROR when the input buffer cap is exceeded', async () => {
|
|
279
|
+
// A peer streaming > 8 KiB with no line terminator would grow the
|
|
280
|
+
// LineScanner carry unboundedly; the server must answer with the
|
|
281
|
+
// RFC-style ERROR notice, destroy the socket, and unregister the
|
|
282
|
+
// connection from the runtime.
|
|
283
|
+
const client = new TcpTestClient('127.0.0.1', server.tcpPort as number);
|
|
284
|
+
await client.opened();
|
|
285
|
+
await client.send('NICK overflower');
|
|
286
|
+
await client.send('USER overflower 0 * :Over');
|
|
287
|
+
await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
|
|
288
|
+
|
|
289
|
+
const connId = await server.runtime.lookupNick('overflower');
|
|
290
|
+
expect(connId).not.toBeNull();
|
|
291
|
+
expect(server.runtime.hasConnection(connId as string)).toBe(true);
|
|
292
|
+
|
|
293
|
+
const closed = new Promise<void>((resolve) => client.socket.once('close', () => resolve()));
|
|
294
|
+
await client.writeRaw('x'.repeat(9 * 1024)); // no terminator anywhere
|
|
295
|
+
const errorLine = await client.waitFor(
|
|
296
|
+
(l) => l === 'ERROR :Closing link: input buffer overflow',
|
|
297
|
+
2_000,
|
|
298
|
+
);
|
|
299
|
+
expect(errorLine).toBe('ERROR :Closing link: input buffer overflow');
|
|
300
|
+
await closed;
|
|
301
|
+
expect(client.socket.destroyed).toBe(true);
|
|
302
|
+
// Cleanup ran: the runtime no longer tracks the connection.
|
|
303
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
304
|
+
expect(server.runtime.hasConnection(connId as string)).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
|
|
278
307
|
it('logs an actor failure on the TCP path without crashing the server', async () => {
|
|
279
308
|
const original = server.runtime.send.bind(server.runtime);
|
|
280
309
|
server.runtime.send = async () => {
|
package/apps/web/package.json
CHANGED
package/docs/AWS-Deployment.md
CHANGED
|
@@ -381,10 +381,12 @@ For a real production deployment, before your first `pnpm deploy:aws`:
|
|
|
381
381
|
1. **Use a dedicated AWS account** (or at least a dedicated region) for
|
|
382
382
|
production. Do not point production credentials at the same stack
|
|
383
383
|
CI is deploying to on every push.
|
|
384
|
-
2. **
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
384
|
+
2. **Keep the default table protection on.** State-bearing tables
|
|
385
|
+
(`Nicks`, `Services`, `ChannelMeta`, `ChannelMembers`) default to
|
|
386
|
+
`RemovalPolicy.RETAIN` + `deletionProtection: true` + PITR-on, so a
|
|
387
|
+
mistaken `cdk destroy` or a stray `aws dynamodb delete-table` cannot
|
|
388
|
+
wipe them (see §9.4). Only set `-c allowTableDeletion=true` for a
|
|
389
|
+
deliberate teardown.
|
|
388
390
|
3. **Set the server identity** (`SERVER_NAME`, `NETWORK_NAME`,
|
|
389
391
|
`MOTD`) — see §7.1. `SERVER_NAME` is required at boot; the stack
|
|
390
392
|
falls back to `irc.localhost` when the prop is omitted, which is
|
|
@@ -498,6 +500,13 @@ any of them** — to enable one, add an `addEnvironment(...)` line in
|
|
|
498
500
|
| `MAX_LIST_ENTRIES` | int | `maxListEntries` |
|
|
499
501
|
| `QUIT_MESSAGE` | string | `quitMessage` |
|
|
500
502
|
| `SASL_ACCOUNTS` | string | `saslAccounts` |
|
|
503
|
+
| `MAX_CONNECTIONS_PER_IP` | int | `maxConnectionsPerIp` |
|
|
504
|
+
| `PER_IP_CONNECTION_RATE_MAX` | int | `perIpConnectionRate.max` |
|
|
505
|
+
| `PER_IP_CONNECTION_RATE_WINDOW_MS` | int | `perIpConnectionRate.windowMs` |
|
|
506
|
+
|
|
507
|
+
The last three are the per-IP admission knobs (§7.8): setting only one
|
|
508
|
+
half of the rate pair falls back to the schema default (5 per 60 000 ms)
|
|
509
|
+
for the other half.
|
|
501
510
|
|
|
502
511
|
`SASL_ACCOUNTS` is the legacy/config fallback (newline-delimited
|
|
503
512
|
`username:password` pairs); it is used only when the `Accounts` table is
|
|
@@ -587,6 +596,51 @@ intentional — it cannot be set by accident. Tear the sandbox stack down
|
|
|
587
596
|
immediately after debugging; never deploy this combination to a shared
|
|
588
597
|
account.
|
|
589
598
|
|
|
599
|
+
### 7.7 DynamoDB table-protection context flags
|
|
600
|
+
|
|
601
|
+
Two CDK context flags control the state-bearing tables' data-loss
|
|
602
|
+
protection (`Nicks`, `Services`, `ChannelMeta`, `ChannelMembers`).
|
|
603
|
+
`Connections` is always `DESTROY` with no PITR and is unaffected by
|
|
604
|
+
either flag. See §9.4 for the full table and the rationale (and the
|
|
605
|
+
"no snapshot-on-delete" note).
|
|
606
|
+
|
|
607
|
+
| Context flag | Default | Effect when flipped |
|
|
608
|
+
|------------------------|---------|-------------------------------------------------------------------------------------|
|
|
609
|
+
| `allowTableDeletion` | `false` | `true` → state tables become `RemovalPolicy.DESTROY` + `deletionProtection: false`, so a deliberate `cdk destroy` teardown can delete them. Data is irreversibly lost. |
|
|
610
|
+
| `enablePitr` | `true` | `false` → point-in-time recovery is disabled on every state table. Independent of `allowTableDeletion` (a teardown keeps PITR on). PITR costs ~$0.20/GB-month per table (§11.1). |
|
|
611
|
+
|
|
612
|
+
Both arrive as strings on the CLI and are normalised with the shared
|
|
613
|
+
`parseContextBool` helper (same path as `allowDataTrace`), so
|
|
614
|
+
`-c allowTableDeletion=false` stays false-y. They map onto the
|
|
615
|
+
`IrcStackProps.allowTableDeletion` / `IrcStackProps.enablePitr` construct
|
|
616
|
+
props (`apps/aws-stack/bin/aws.ts`).
|
|
617
|
+
|
|
618
|
+
### 7.8 Connect throttling (per-IP admission, stage limits, WAF)
|
|
619
|
+
|
|
620
|
+
Connection setup is where credential brute-force spends its budget
|
|
621
|
+
(`PASS` / `AUTHENTICATE` per connection), so the stack throttles it in
|
|
622
|
+
three layers, outermost first:
|
|
623
|
+
|
|
624
|
+
| Layer | Where | Knob | Default |
|
|
625
|
+
|-------|-------|------|---------|
|
|
626
|
+
| **1. WAF edge rate limit** | WAFv2 rate-based rule, associated with the stage. WAF only inspects the initial HTTP upgrade on a WebSocket API, so the rule is effectively scoped to `$connect` and never re-fires on established frames. | `-c wafConnectRateLimit=<req/5min/IP>` (CDK context; blocks at the edge before a Lambda invocation is billed) | off (no WAF resources) |
|
|
627
|
+
| **2. APIGW stage throttling** | `defaultRouteSettings` on the stage — global backstop across ALL routes and IPs. | `--parameters ApiThrottlingRateLimit=…` / `--parameters ApiThrottlingBurstLimit=…` (CloudFormation parameters) | 100 rps / burst 200 |
|
|
628
|
+
| **3. Per-IP admission gates** | Inside the `$connect` Lambda: a paginated Query against the `Connections` table's `sourceIp-connectedSince` GSI counts the caller IP's live connections (`maxConnectionsPerIp`) and in-window establishments (`perIpConnectionRate`); over-budget connects return `429` and no row is written. The same rate budget gates NLB new flows (§AWS-TCP-Deployment). | `MAX_CONNECTIONS_PER_IP`, `PER_IP_CONNECTION_RATE_MAX`, `PER_IP_CONNECTION_RATE_WINDOW_MS` env vars (§7.3) | 10 / 5 per 60 s |
|
|
629
|
+
|
|
630
|
+
Notes:
|
|
631
|
+
|
|
632
|
+
- Layer 1 is opt-in because WAFv2 web ACLs cost ~$5/mo + $1/rule + $0.60/M
|
|
633
|
+
requests; for a small deployment, layers 2+3 are usually enough. Enable
|
|
634
|
+
with `pnpm --filter @serverless-ircd/aws-stack run cdk:synth -- -c wafConnectRateLimit=2000`
|
|
635
|
+
(2000 upgrade requests per 5-minute window per client IP before the IP
|
|
636
|
+
is blocked at the edge).
|
|
637
|
+
- Layer 3's per-IP counts are eventually consistent (GSI reads), so a
|
|
638
|
+
coordinated burst from one IP can slightly overshoot the cap — the same
|
|
639
|
+
best-effort TOCTOU contract as the global `maxClients` counter.
|
|
640
|
+
- The 429 response carries `ERROR :Closing link` semantics at the HTTP
|
|
641
|
+
layer where possible; clients should treat it as back-pressure (retry
|
|
642
|
+
after the window decays), not a permanent ban.
|
|
643
|
+
|
|
590
644
|
---
|
|
591
645
|
|
|
592
646
|
## 8. Secrets
|
|
@@ -855,28 +909,62 @@ postToConnection, per-channel send-list cache, a dedicated fanout
|
|
|
855
909
|
Lambda) are deferred to post-v1; the load-test ticket will surface
|
|
856
910
|
the ceiling.
|
|
857
911
|
|
|
858
|
-
### 9.4 Removal policy (IMPORTANT)
|
|
912
|
+
### 9.4 Removal policy, deletion protection, and PITR (IMPORTANT)
|
|
913
|
+
|
|
914
|
+
Every table is protected from accidental data loss by default. The
|
|
915
|
+
state-bearing tables (`Nicks`, `Services`, `ChannelMeta`, `ChannelMembers`)
|
|
916
|
+
get **three independent safeguards**, each toggleable so a deliberate
|
|
917
|
+
teardown can still proceed; the one ephemeral table (`Connections`) is
|
|
918
|
+
always DESTROY:
|
|
919
|
+
|
|
920
|
+
| Table | RemovalPolicy | `deletionProtection` | PITR |
|
|
921
|
+
|-----------------------------------------------|---------------|----------------------|------|
|
|
922
|
+
| `Nicks`, `Services`, `ChannelMeta`, `ChannelMembers` | `RETAIN` (default) | `true` (default) | `true` (default) |
|
|
923
|
+
| `Connections` | `DESTROY` | (unset → off) | (unset → off) |
|
|
924
|
+
|
|
925
|
+
- **`RemovalPolicy.RETAIN`** — when the stack is deleted, CloudFormation
|
|
926
|
+
*orphans* the table instead of deleting it; the data survives.
|
|
927
|
+
- **`deletionProtection: true`** — DynamoDB refuses a `DeleteTable` API
|
|
928
|
+
call while this is on, blocking a stray `aws dynamodb delete-table` even
|
|
929
|
+
from an admin or compromised credential.
|
|
930
|
+
- **`pointInTimeRecovery` (PITR)** — continuous, per-table restore (within
|
|
931
|
+
the last ~35 days) against accidental writes/deletes that RETAIN +
|
|
932
|
+
deletion protection cannot stop (e.g. a buggy `UpdateItem` that
|
|
933
|
+
overwrites rows).
|
|
934
|
+
|
|
935
|
+
> **No snapshot-on-delete.** DynamoDB tables do NOT snapshot when a stack
|
|
936
|
+
> is deleted (`RemovalPolicy.SNAPSHOT` is a no-op here). `RETAIN` (orphan +
|
|
937
|
+
> keep) + PITR (continuous, in-place restore) is the correct combination;
|
|
938
|
+
> do not rely on a snapshot that does not exist.
|
|
939
|
+
|
|
940
|
+
Two CDK context flags control these (threaded through `bin/aws.ts`, see
|
|
941
|
+
§7.7):
|
|
942
|
+
|
|
943
|
+
- **`allowTableDeletion`** (default `false`). Set `-c allowTableDeletion=true`
|
|
944
|
+
for a deliberate `cdk destroy` teardown: every state table flips to
|
|
945
|
+
`RemovalPolicy.DESTROY` with `deletionProtection: false` so CloudFormation
|
|
946
|
+
can delete them. The data is **irreversibly lost** — flip this off only
|
|
947
|
+
when you genuinely want the tables gone. `Connections` is always DESTROY
|
|
948
|
+
regardless.
|
|
949
|
+
- **`enablePitr`** (default `true`). Set `-c enablePitr=false` to disable
|
|
950
|
+
PITR on every state table. Independent of `allowTableDeletion` — a
|
|
951
|
+
teardown keeps PITR on (harmless, and the data is still recoverable up
|
|
952
|
+
until the table is actually deleted). `Connections` never carries PITR.
|
|
859
953
|
|
|
860
|
-
|
|
954
|
+
```bash
|
|
955
|
+
# Default deploy: state tables RETAIN + deletion-protected + PITR-on.
|
|
956
|
+
pnpm --filter @serverless-ircd/aws-stack run cdk:synth
|
|
861
957
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const tables = Object.entries(TABLE_DEFS).map(
|
|
865
|
-
([logicalId, tableProps]) =>
|
|
866
|
-
new Table(this, logicalId, { ...tableProps, removalPolicy: RemovalPolicy.DESTROY }),
|
|
867
|
-
);
|
|
868
|
-
```
|
|
958
|
+
# Deliberate teardown (state tables become DELETABLE):
|
|
959
|
+
pnpm --filter @serverless-ircd/aws-stack run cdk:synth -- -c allowTableDeletion=true
|
|
869
960
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
961
|
+
# Disable PITR (e.g. cost-sensitive dev deploy):
|
|
962
|
+
pnpm --filter @serverless-ircd/aws-stack run cdk:synth -- -c enablePitr=false
|
|
963
|
+
```
|
|
873
964
|
|
|
874
|
-
|
|
875
|
-
`
|
|
876
|
-
|
|
877
|
-
editing `aws-stack.ts`. Recommendation: ship a `production: boolean`
|
|
878
|
-
stack prop in a future change that flips the removal policy. Flagged
|
|
879
|
-
in §15.
|
|
965
|
+
The protection knobs are asserted per parameter combination in
|
|
966
|
+
`apps/aws-stack/tests/stack.test.ts` (the "DynamoDB table protection"
|
|
967
|
+
suite).
|
|
880
968
|
|
|
881
969
|
### 9.5 TTL on `Connections.idleSince`
|
|
882
970
|
|
|
@@ -1005,12 +1093,25 @@ verify against the current AWS pricing page** before budgeting:
|
|
|
1005
1093
|
| Lambda | GB-second | $0.0000166667 (≈ $0.0167 / GB-hour) |
|
|
1006
1094
|
| DynamoDB on-demand | write request unit (WRU) | $1.25 / million |
|
|
1007
1095
|
| DynamoDB on-demand | read request unit (RRU) | $0.25 / million |
|
|
1096
|
+
| DynamoDB PITR (continuous)| GB-month of table data + indexes, per table | ~$0.20 / GB-month |
|
|
1008
1097
|
| EventBridge Scheduler | event published | $1.00 / million |
|
|
1009
1098
|
| CloudWatch Logs | GB ingested | $0.50 |
|
|
1010
1099
|
|
|
1011
1100
|
The Lambda free tier (1M requests + 400k GB-seconds per month) covers
|
|
1012
1101
|
a meaningful slice of a small staging deployment.
|
|
1013
1102
|
|
|
1103
|
+
> **Table-protection cost.** `RemovalPolicy.RETAIN` and
|
|
1104
|
+
> `deletionProtection` are **free** — they are CloudFormation/DynamoDB
|
|
1105
|
+
> flags, not billable features. Point-in-time recovery (PITR) **is**
|
|
1106
|
+
> billed: continuously at ~$0.20/GB-month **per table**, charged on the
|
|
1107
|
+
> full table size including indexes, for as long as PITR is on. With the
|
|
1108
|
+
> default (`enablePitr=true`) this applies to the four state tables
|
|
1109
|
+
> (`Nicks`, `Services`, `ChannelMeta`, `ChannelMembers`); `Connections`
|
|
1110
|
+
> never carries PITR. For a small deployment these tables are tiny
|
|
1111
|
+
> (kilobytes to low megabytes), so the PITR line item is fractions of a
|
|
1112
|
+
> cent; it only becomes material if a table grows to many GB. Disable
|
|
1113
|
+
> with `-c enablePitr=false` on a cost-sensitive dev deploy (see §9.4).
|
|
1114
|
+
|
|
1014
1115
|
### 11.2 Back-of-envelope: 100 concurrent users
|
|
1015
1116
|
|
|
1016
1117
|
Assume a small staging deployment with 100 concurrent users, average
|
|
@@ -212,6 +212,18 @@ State lifecycle is identical to the wss path: the **sweeper** Lambda
|
|
|
212
212
|
no-PONG. Both cover TCP connections because the `connId` lives in the
|
|
213
213
|
same `Connections` table.
|
|
214
214
|
|
|
215
|
+
**Per-IP new-flow rate limit.** The NLB path has no `$connect` event —
|
|
216
|
+
flow establishment IS the first chunk — so the `perIpConnectionRate`
|
|
217
|
+
budget is enforced inside `handleNlbStream`: the source IP (from
|
|
218
|
+
`x-forwarded-for`) is counted against the same `sourceIp-connectedSince`
|
|
219
|
+
GSI the wss `$connect` gate consults, and over-budget flows receive
|
|
220
|
+
`429` with no row written. Only establishment is budgeted: every
|
|
221
|
+
subsequent chunk of an established flow is also an invocation, and
|
|
222
|
+
rate-limiting those would starve an active IRC session. The knobs are
|
|
223
|
+
the shared `PER_IP_CONNECTION_RATE_MAX` / `PER_IP_CONNECTION_RATE_WINDOW_MS`
|
|
224
|
+
env vars (default 5 per 60 s; see `AWS-Deployment.md` §7.8 for the
|
|
225
|
+
three-layer throttling model).
|
|
226
|
+
|
|
215
227
|
---
|
|
216
228
|
|
|
217
229
|
## 7. Configuration reference
|
|
@@ -230,7 +242,11 @@ wss handler (`serverName`, `networkName`, `motd`), injected in
|
|
|
230
242
|
- `SERVER_NAME`, `NETWORK_NAME`, `MOTD` (read by the shared
|
|
231
243
|
`config-loader.ts`).
|
|
232
244
|
- `MANAGEMENT_URL` — the wss stage's callback URL, so cross-connection
|
|
233
|
-
fanout reaches recipients on either transport.
|
|
245
|
+
fanout reaches recipients on either transport. The NLB handler's
|
|
246
|
+
execution role also carries `execute-api:ManageConnections` on the
|
|
247
|
+
wss stage (`stage.grantManagementApiAccess(nlbHandler)` in
|
|
248
|
+
`aws-stack.ts`, asserted by a synth-time test) — without it every
|
|
249
|
+
cross-transport `postToConnection` fails with 403.
|
|
234
250
|
- `<TABLE>_TABLE` for each of the five tables (bare logical ids).
|
|
235
251
|
|
|
236
252
|
The Lambda handler entry is the same file
|
|
@@ -240,7 +256,10 @@ excluded from the bundle (ships with the runtime).
|
|
|
240
256
|
|
|
241
257
|
There are **no TCP-specific DynamoDB tables** — the existing `Connections`
|
|
242
258
|
table gains a `transportBuffer` string attribute (SET when non-empty,
|
|
243
|
-
REMOVE when empty) on rows whose `connectionId` starts with `nlb
|
|
259
|
+
REMOVE when empty) on rows whose `connectionId` starts with `nlb-`, plus
|
|
260
|
+
the `sourceIp` attribute + `sourceIp-connectedSince` GSI shared with the
|
|
261
|
+
wss path's per-IP admission gates (both transports stamp the flow/client
|
|
262
|
+
IP at establishment).
|
|
244
263
|
|
|
245
264
|
---
|
|
246
265
|
|
|
@@ -306,6 +325,22 @@ then fanout uses `ApiGatewayManagementApi.postToConnection`, which works
|
|
|
306
325
|
for both transports. If `MANAGEMENT_URL` is unset, `managementApi` is
|
|
307
326
|
`null` and cross-connection sends are silent no-ops.
|
|
308
327
|
|
|
328
|
+
The mirror-image symptom — a TCP client's messages never reaching wss
|
|
329
|
+
recipients — is an IAM problem, not a push problem: the NLB handler's
|
|
330
|
+
role must carry `execute-api:ManageConnections` on the wss stage
|
|
331
|
+
(`stage.grantManagementApiAccess(nlbHandler)`; synth-time asserted in
|
|
332
|
+
`apps/aws-stack/tests/stack.test.ts`). Without the grant the NLB
|
|
333
|
+
handler's `postToConnection` calls fail with 403 and the delivery is
|
|
334
|
+
silently dropped. Verify manually after a deploy:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# 1. Connect over wss (e.g. the web client) and JOIN #fanout-check.
|
|
338
|
+
# 2. Connect over TLS :6697 (openssl one-liner in §11) and JOIN #fanout-check.
|
|
339
|
+
# 3. PRIVMSG from the TCP side — the wss client must receive it, and vice
|
|
340
|
+
# versa. A silent drop + 403 in the IrcNlbHandler CloudWatch logs
|
|
341
|
+
# means the ManageConnections grant is missing.
|
|
342
|
+
```
|
|
343
|
+
|
|
309
344
|
### 9.4 Lurkers get disconnected after a few minutes
|
|
310
345
|
|
|
311
346
|
The NLB idle timeout. A receiving-only client sends no chunks, so the
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Chat History (`draft/chathistory`)
|
|
2
|
+
|
|
3
|
+
The server implements the IRCv3 [`draft/chathistory`](https://ircv3.net/specs/extensions/chathistory)
|
|
4
|
+
extension: a client that negotiates the `draft/chathistory` cap may query
|
|
5
|
+
stored backlog (`LATEST` / `BEFORE` / `AFTER` / `AROUND` / `BETWEEN` /
|
|
6
|
+
`TARGETS`) and receives the matching messages wrapped in a
|
|
7
|
+
`BATCH chathistory <target>` frame, each replay line carrying its original
|
|
8
|
+
`@time=…` and `msgid=…` tags.
|
|
9
|
+
|
|
10
|
+
## Query limit ceiling
|
|
11
|
+
|
|
12
|
+
Every client-supplied `limit` is **silently capped** at a deployment
|
|
13
|
+
ceiling before it reaches the `MessageStore`:
|
|
14
|
+
|
|
15
|
+
- `CHATHISTORY LATEST #chan * 999999999` returns at most
|
|
16
|
+
`chathistory.maxLimit` messages (default **100**) — the oversized value
|
|
17
|
+
never reaches the store, so a future higher-capacity backend cannot be
|
|
18
|
+
tricked into materializing a huge result set.
|
|
19
|
+
- The cap is silent: the client receives up to the ceiling worth of
|
|
20
|
+
replay lines and no error. Requests below the ceiling pass through
|
|
21
|
+
unchanged; a request for exactly the ceiling is honored in full.
|
|
22
|
+
- When the client omits the limit, the server default (50) applies —
|
|
23
|
+
also bounded by the ceiling, so a deployment that lowers
|
|
24
|
+
`chathistory.maxLimit` below 50 caps the default too.
|
|
25
|
+
- A limit that is not a non-negative integer (e.g. `abc`, `-5`) is
|
|
26
|
+
rejected with `461 ERR_NEEDMOREPARAMS` (Invalid parameters), unchanged
|
|
27
|
+
by the ceiling.
|
|
28
|
+
|
|
29
|
+
### Configuration
|
|
30
|
+
|
|
31
|
+
| Knob | Default | Notes |
|
|
32
|
+
|-----------------------------|---------|------------------------------------------------|
|
|
33
|
+
| `chathistory.maxLimit` | `100` | Positive integer; per-`CHATHISTORY`-query cap. |
|
|
34
|
+
|
|
35
|
+
The knob lives in the server config (parsed by `ServerConfigSchema`), so
|
|
36
|
+
any adapter's config source can set it:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"serverName": "irc.example.com",
|
|
41
|
+
"networkName": "ExampleNet",
|
|
42
|
+
"chathistory": { "maxLimit": 250 }
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Omitting the `chathistory` section entirely inherits the default of 100.
|
|
47
|
+
|
|
48
|
+
## Related
|
|
49
|
+
|
|
50
|
+
- JOIN auto-playback (`chatHistoryPlaybackLimit`, default 50) is a
|
|
51
|
+
separate knob covering the backlog replayed on JOIN; see
|
|
52
|
+
`packages/irc-core/src/config.ts`.
|
|
53
|
+
- `draft/read-marker` integration: `CHATHISTORY BEFORE` without an
|
|
54
|
+
explicit pivot falls back to the connection's last-read marker; see
|
|
55
|
+
`docs/PlanExtensions.md`.
|
|
@@ -287,6 +287,10 @@ is the single source of truth for the Worker.
|
|
|
287
287
|
| `SERVER_NAME` | Server name sent in `001`/`005` numerics. **Required** — the CF config loader fails fast at boot when unset. | _No default; must be set in `wrangler.toml`._ |
|
|
288
288
|
| `NETWORK_NAME` | Network label in `005 NETWORK=…`. | `ServerlessIRCd` |
|
|
289
289
|
| `MOTD_LINES` | Message-of-the-day, `\n`-delimited. | Welcome banner string. |
|
|
290
|
+
| `MAX_CLIENTS` | Global live-connection cap (enforced by `CounterDO` at the edge). | schema default |
|
|
291
|
+
| `MAX_CONNECTIONS_PER_IP` | Per-IP simultaneous-connection cap (in-memory admission in `ConnectionDO`). | schema default (10) |
|
|
292
|
+
| `PER_IP_CONNECTION_RATE_MAX` | Per-IP upgrade rate budget — how many WebSocket upgrades one `CF-Connecting-IP` may open per sliding window. Enforced at the edge by `RateLimitDO` (429 when over budget). | schema default (5) |
|
|
293
|
+
| `PER_IP_CONNECTION_RATE_WINDOW_MS` | Sliding-window length in ms for the upgrade rate budget. Setting only one half of the pair falls back to the schema default (60 000 ms) for the other. | schema default (60 000) |
|
|
290
294
|
|
|
291
295
|
These are read by `ConnectionDO` via `Env` (see
|
|
292
296
|
`packages/cf-adapter/src/env.ts:20` and the `serverConfig()` method in
|
|
@@ -295,7 +299,7 @@ bundle and are visible in the dashboard.
|
|
|
295
299
|
|
|
296
300
|
### 7.2 Durable Object bindings
|
|
297
301
|
|
|
298
|
-
|
|
302
|
+
The DO classes are exported from `src/worker.ts` (re-exported from
|
|
299
303
|
`@serverless-ircd/cf-adapter`) and bound in `wrangler.toml`:
|
|
300
304
|
|
|
301
305
|
| Binding name | Class | Owned state |
|
|
@@ -303,6 +307,9 @@ Three DO classes are exported from `src/worker.ts` (re-exported from
|
|
|
303
307
|
| `CONNECTION_DO` | `ConnectionDO` | Socket + ConnectionState + PING alarms. |
|
|
304
308
|
| `REGISTRY_DO` | `RegistryDO` | Nick uniqueness, nick→connId map (sharded).|
|
|
305
309
|
| `CHANNEL_DO` | `ChannelDO` | Per-channel roster, modes, topic, fanout. |
|
|
310
|
+
| `CHANNEL_REGISTRY_DO` | `ChannelRegistryDO` | The channel-name registry (shard index). |
|
|
311
|
+
| `COUNTER_DO` | `CounterDO` | Global live-connection counter (`maxClients` cap, TICKET-194). |
|
|
312
|
+
| `RATE_LIMIT_DO` | `RateLimitDO` | Per-IP upgrade rate limiter — one sliding-window admission list per `CF-Connecting-IP`; the Worker edge checks the budget before forwarding an upgrade and rejects over-budget IPs with `429` (the per-IP layer of the connect throttling; runs before the `maxClients` counter so a flooding IP cannot consume global slots). |
|
|
306
313
|
|
|
307
314
|
The class names in `wrangler.toml`'s `class_name` field MUST match the
|
|
308
315
|
re-exports at the top of `apps/cf-worker/src/worker.ts:29`.
|
|
@@ -312,7 +319,7 @@ re-exports at the top of `apps/cf-worker/src/worker.ts:29`.
|
|
|
312
319
|
```toml
|
|
313
320
|
[[migrations]]
|
|
314
321
|
tag = "v1"
|
|
315
|
-
new_sqlite_classes = ["ConnectionDO", "RegistryDO", "ChannelDO", "ChannelRegistryDO"]
|
|
322
|
+
new_sqlite_classes = ["ConnectionDO", "RegistryDO", "ChannelDO", "ChannelRegistryDO", "CounterDO", "RateLimitDO"]
|
|
316
323
|
```
|
|
317
324
|
|
|
318
325
|
`tag` is the migration id (string, monotonically tracked by wrangler).
|