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/apps/aws-stack/cdk.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/aws-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "AWS CDK v2 stack: API Gateway v2 WebSocket API, Lambda, DynamoDB tables, least-privilege IAM",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
"clean": "rimraf dist coverage .tsbuildinfo .turbo cdk.out",
|
|
15
15
|
"cdk": "cdk",
|
|
16
16
|
"cdk:synth": "cdk synth",
|
|
17
|
-
"deploy
|
|
18
|
-
"
|
|
19
|
-
"smoke:staging": "node scripts/smoke.mjs"
|
|
17
|
+
"deploy": "cdk deploy --all --require-approval never",
|
|
18
|
+
"smoke": "node scripts/smoke.mjs"
|
|
20
19
|
},
|
|
21
20
|
"devDependencies": {
|
|
22
21
|
"aws-cdk": "^2.262.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `apps/aws-stack` CDK stack — the AWS substrate for ServerlessIRCd (PLAN §6.2).
|
|
3
3
|
*
|
|
4
4
|
* • 5 DynamoDB tables sourced from `@serverless-ircd/aws-adapter/cdk-table-defs`
|
|
5
|
-
* (Connections, ChannelMeta, ChannelMembers, Nicks,
|
|
5
|
+
* (Connections, ChannelMeta, ChannelMembers, Nicks, Services).
|
|
6
6
|
* • ONE Node 24 `NodejsFunction` bundling the AWS Lambda handler
|
|
7
7
|
* (`$connect` / `$disconnect` / `$default` dispatched on
|
|
8
8
|
* `event.requestContext.routeKey`).
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from 'aws-cdk-lib/aws-apigatewayv2';
|
|
35
35
|
import { WebSocketLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
|
36
36
|
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
37
|
-
import { Table } from 'aws-cdk-lib/aws-dynamodb';
|
|
37
|
+
import { Table, type TableProps } from 'aws-cdk-lib/aws-dynamodb';
|
|
38
38
|
import { Vpc } from 'aws-cdk-lib/aws-ec2';
|
|
39
39
|
import {
|
|
40
40
|
NetworkListener,
|
|
@@ -50,7 +50,9 @@ import { PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
|
|
|
50
50
|
import { Code, Function as Lambda, Runtime } from 'aws-cdk-lib/aws-lambda';
|
|
51
51
|
import { NodejsFunction, type NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
52
52
|
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
|
|
53
|
+
import { CfnWebACL, CfnWebACLAssociation } from 'aws-cdk-lib/aws-wafv2';
|
|
53
54
|
import type { Construct } from 'constructs';
|
|
55
|
+
import { StaticSite, type StaticSiteProps } from './static-site.js';
|
|
54
56
|
|
|
55
57
|
/** Stage name served by the auto-deploying WebSocket stage. */
|
|
56
58
|
const STAGE_NAME = 'prod';
|
|
@@ -60,7 +62,14 @@ const IRC_TLS_PORT = 6697;
|
|
|
60
62
|
|
|
61
63
|
/** Fixed rate for the gone-connection sweeper (matches the architecture doc). */
|
|
62
64
|
const SWEEPER_SCHEDULE_RATE = Duration.minutes(5);
|
|
63
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Logical id of the one ephemeral DynamoDB table. `Connections` holds
|
|
67
|
+
* per-connection rows (TTL-reaped, sweeper-cleaned) and is the ONLY table
|
|
68
|
+
* kept on `RemovalPolicy.DESTROY` with no PITR and no deletion protection;
|
|
69
|
+
* every other table is state-bearing and protected (see the table-creation
|
|
70
|
+
* block below).
|
|
71
|
+
*/
|
|
72
|
+
const EPHEMERAL_TABLE_LOGICAL_ID = 'Connections';
|
|
64
73
|
/**
|
|
65
74
|
* Fixed rate for the idle / PING checker. One minute is the finest
|
|
66
75
|
* granularity EventBridge `rate(...)` expressions support, which
|
|
@@ -68,13 +77,6 @@ const SWEEPER_SCHEDULE_RATE = Duration.minutes(5);
|
|
|
68
77
|
*/
|
|
69
78
|
const PING_CHECKER_SCHEDULE_RATE = Duration.minutes(1);
|
|
70
79
|
|
|
71
|
-
/**
|
|
72
|
-
* Environment assumed when {@link IrcStackProps.environmentName} is
|
|
73
|
-
* omitted. Keeps the existing deploy pipeline green without requiring
|
|
74
|
-
* callers to pass the flag.
|
|
75
|
-
*/
|
|
76
|
-
const DEFAULT_ENVIRONMENT_NAME = 'staging';
|
|
77
|
-
|
|
78
80
|
/**
|
|
79
81
|
* Fallback server identity, used when the corresponding prop is omitted.
|
|
80
82
|
* Deliberately not `irc.example.com` (the retired placeholder) so a
|
|
@@ -109,21 +111,10 @@ const TEST_NO_BUNDLE_FLAG = 'IRC_AWS_STACK_TEST_NO_BUNDLE';
|
|
|
109
111
|
|
|
110
112
|
/**
|
|
111
113
|
* Construct props for {@link IrcAwsStack}. Acts as the single
|
|
112
|
-
* configuration bag extended by deploy-time knobs (
|
|
113
|
-
*
|
|
114
|
+
* configuration bag extended by deploy-time knobs (server identity, …)
|
|
115
|
+
* so the prop surface grows in one place.
|
|
114
116
|
*/
|
|
115
117
|
export interface IrcStackProps extends StackProps {
|
|
116
|
-
/**
|
|
117
|
-
* Logical deployment environment (e.g. `staging`, `production`).
|
|
118
|
-
*
|
|
119
|
-
* Prefixes every physical DynamoDB table name
|
|
120
|
-
* (`<Env><LogicalId>` — first letter capitalised, e.g.
|
|
121
|
-
* `StagingConnections`) and is intended to be folded into the
|
|
122
|
-
* CloudFormation stack id by the caller
|
|
123
|
-
* (`IrcAwsStack-<environmentName>`). Defaults to `staging`.
|
|
124
|
-
*/
|
|
125
|
-
readonly environmentName?: string;
|
|
126
|
-
|
|
127
118
|
/**
|
|
128
119
|
* Server name advertised to clients (`SERVER_NAME` / the `001`
|
|
129
120
|
* welcome source). Falls back to `irc.localhost` when omitted; production
|
|
@@ -158,13 +149,148 @@ export interface IrcStackProps extends StackProps {
|
|
|
158
149
|
* (WebSocket) transport is deployed.
|
|
159
150
|
*/
|
|
160
151
|
readonly tcpTlsDomainName?: string;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Web client hosting substrate (S3 + CloudFront + OAC).
|
|
155
|
+
*
|
|
156
|
+
* When provided, the stack provisions a {@link StaticSite} that serves
|
|
157
|
+
* the shared `apps/web/dist` build artifact over CloudFront. The S3
|
|
158
|
+
* origin bucket is private and grants the distribution via Origin
|
|
159
|
+
* Access Control (never public-read); SPA client-side routing under
|
|
160
|
+
* `/app/*` is handled by a CloudFront custom error response mapping
|
|
161
|
+
* 403/404 → `200 /app/index.html`. Pure infrastructure — the per-env
|
|
162
|
+
* config bake and the s3-sync deploy pipeline are separate. Pass the
|
|
163
|
+
* custom-domain fields for prod; omit them for the default
|
|
164
|
+
* `*.cloudfront.net` URL (staging). When omitted entirely, no web
|
|
165
|
+
* hosting resources are provisioned.
|
|
166
|
+
*/
|
|
167
|
+
readonly webSite?: StaticSiteProps;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Comma-separated allowlist of web origins permitted to open
|
|
171
|
+
* WebSocket upgrades (CSWSH defence — API Gateway WebSocket has no
|
|
172
|
+
* built-in Origin check). Mirrors the CF Worker's `WEB_ORIGINS` env.
|
|
173
|
+
*
|
|
174
|
+
* When provided (non-empty), the value is injected as the
|
|
175
|
+
* `WEB_ORIGINS` Lambda env var on the wss `$connect` handler. The
|
|
176
|
+
* `$connect` Lambda then denies any browser-sent `Origin` not in
|
|
177
|
+
* the list with `403`, so API Gateway closes the upgrade. Unset /
|
|
178
|
+
* empty (the default) disables the check — existing deployments
|
|
179
|
+
* without a web frontend are unchanged on upgrade. Non-browser
|
|
180
|
+
* clients (curl, WeeChat, the `tcp-ws-forwarder`, NLB TCP+TLS)
|
|
181
|
+
* never send `Origin` and pass through regardless.
|
|
182
|
+
*
|
|
183
|
+
* Example: `'https://app.example.com, https://staging.app.example.com'`
|
|
184
|
+
*/
|
|
185
|
+
readonly webOrigins?: string;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Escape hatch that re-enables API Gateway full-frame body tracing
|
|
189
|
+
* (`DataTraceEnabled = true`) on the WebSocket stage. Defaults to
|
|
190
|
+
* `false` (safe: no frame bodies are logged).
|
|
191
|
+
*
|
|
192
|
+
* ⚠️ Setting this to `true` alone REFUSES synthesis — it must be
|
|
193
|
+
* accompanied by {@link iUnderstandThisLeaksCredentials} = `true`.
|
|
194
|
+
* This double opt-in is deliberate: since the staging/prod split was
|
|
195
|
+
* collapsed (every synth is effectively a prod synth; there is no
|
|
196
|
+
* `environmentName` context), there is no longer a "non-prod" branch
|
|
197
|
+
* to quietly enable tracing on. The only safe default is OFF, and the
|
|
198
|
+
* only way back on is an explicit, two-step acknowledgement.
|
|
199
|
+
*
|
|
200
|
+
* For a WebSocket IRC endpoint, body tracing writes the FULL payload
|
|
201
|
+
* of every frame — `PASS <password>`, `AUTHENTICATE <base64-SASL-PLAIN>`,
|
|
202
|
+
* `JOIN #chan <channel-key>`, and every `PRIVMSG`/`NOTICE` — to the
|
|
203
|
+
* APIGW execution-log CloudWatch group. Anyone with `logs:GetLogEvents`
|
|
204
|
+
* (operators, SREs, Logpush destinations, compromised CI creds)
|
|
205
|
+
* recovers IRC credentials verbatim. Use ONLY in a throwaway sandbox
|
|
206
|
+
* account for live debugging, and tear it down immediately after.
|
|
207
|
+
*
|
|
208
|
+
* Set via CDK context: `-c allowDataTrace=true`.
|
|
209
|
+
*/
|
|
210
|
+
readonly allowDataTrace?: boolean;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Mandatory confirmation that accompanies {@link allowDataTrace} = `true`.
|
|
214
|
+
* Has NO effect on its own; it only unlocks the `allowDataTrace` escape
|
|
215
|
+
* hatch. Defaults to `false`. The awkward name is intentional: it cannot
|
|
216
|
+
* be set by accident, and a synth error names this prop when it is
|
|
217
|
+
* missing so the operator is forced to read the credential-leak warning.
|
|
218
|
+
*
|
|
219
|
+
* Set via CDK context: `-c iUnderstandThisLeaksCredentials=true`.
|
|
220
|
+
*/
|
|
221
|
+
readonly iUnderstandThisLeaksCredentials?: boolean;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Escape hatch that allows `cdk destroy` to tear down the state-bearing
|
|
225
|
+
* DynamoDB tables (Nicks, Services, ChannelMeta, ChannelMembers).
|
|
226
|
+
*
|
|
227
|
+
* Defaults to `false` (safe): state tables are `RemovalPolicy.RETAIN`'d
|
|
228
|
+
* and carry `deletionProtection: true`, so a mistaken `cdk destroy`, a
|
|
229
|
+
* stack-name collision, or a stray `aws dynamodb delete-table` from a
|
|
230
|
+
* compromised credential CANNOT wipe user accounts. The Connections
|
|
231
|
+
* table is always DESTROY regardless (its rows are ephemeral per-
|
|
232
|
+
* connection records, TTL-reaped and sweeper-cleaned).
|
|
233
|
+
*
|
|
234
|
+
* Set to `true` ONLY for a deliberate teardown (`-c allowTableDeletion=true`):
|
|
235
|
+
* every state table flips to `RemovalPolicy.DESTROY` with
|
|
236
|
+
* `deletionProtection: false` so CloudFormation can delete them. The data
|
|
237
|
+
* is irreversibly lost — there is no DynamoDB snapshot-on-delete (tables
|
|
238
|
+
* do not snapshot); `RETAIN` + PITR is the only protection, so flipping
|
|
239
|
+
* this off is a one-way operation. {@link enablePitr} is independent and
|
|
240
|
+
* stays on by default even in teardown mode.
|
|
241
|
+
*/
|
|
242
|
+
readonly allowTableDeletion?: boolean;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Toggles point-in-time recovery (PITR) on the state-bearing DynamoDB
|
|
246
|
+
* tables (Nicks, Services, ChannelMeta, ChannelMembers). Defaults to
|
|
247
|
+
* `true` — PITR enables continuous, per-table point-in-time restore
|
|
248
|
+
* (within the last ~35 days) against accidental writes or deletes that
|
|
249
|
+
* RETAIN + deletion protection do not cover (e.g. a buggy deploy that
|
|
250
|
+
* overwrites rows, or an `UpdateItem` with the wrong key).
|
|
251
|
+
*
|
|
252
|
+
* The Connections table NEVER carries PITR regardless of this flag: its
|
|
253
|
+
* rows are ephemeral and the restore cost is unjustified.
|
|
254
|
+
*
|
|
255
|
+
* Set `-c enablePitr=false` to disable on all tables. PITR is billed
|
|
256
|
+
* continuously at ~$0.20/GB-month per table (see docs/AWS-Deployment.md);
|
|
257
|
+
* RETAIN and deletionProtection are free.
|
|
258
|
+
*/
|
|
259
|
+
readonly enablePitr?: boolean;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Optional WAFv2 per-IP edge rate limit for the WebSocket API's
|
|
263
|
+
* `$connect` route (requests per 5-minute window per client IP).
|
|
264
|
+
*
|
|
265
|
+
* When set, the stack emits a `AWS::WAFv2::WebACL` (REGIONAL scope,
|
|
266
|
+
* default action Allow) whose single rate-based rule BLOCKS an IP once
|
|
267
|
+
* it exceeds the limit, associated with the WebSocket stage. WAF on a
|
|
268
|
+
* WebSocket API only inspects the initial HTTP upgrade — i.e. the
|
|
269
|
+
* `$connect` route; established-connection frames ride the upgrade and
|
|
270
|
+
* are never re-inspected — so the rule is effectively scoped to
|
|
271
|
+
* connection establishment without any route matching.
|
|
272
|
+
*
|
|
273
|
+
* This is the EDGE tier of the three-layer connect throttling (WAF at
|
|
274
|
+
* the edge → APIGW stage throttling → the per-IP admission gates in
|
|
275
|
+
* the `$connect` handler); it blocks a flood before a Lambda is
|
|
276
|
+
* invoked at all. Unset (the default) provisions no WAF resources.
|
|
277
|
+
*
|
|
278
|
+
* Set via CDK context: `-c wafConnectRateLimit=2000`.
|
|
279
|
+
*/
|
|
280
|
+
readonly wafConnectRateLimit?: number;
|
|
161
281
|
}
|
|
162
282
|
|
|
163
283
|
export class IrcAwsStack extends Stack {
|
|
164
284
|
constructor(scope: Construct, id: string, props: IrcStackProps = {}) {
|
|
165
285
|
super(scope, id, props);
|
|
166
286
|
|
|
167
|
-
|
|
287
|
+
// Fail-fast on the credential-leak escape hatch BEFORE any resource is
|
|
288
|
+
// constructed: if `allowDataTrace` was requested without the explicit
|
|
289
|
+
// `iUnderstandThisLeaksCredentials` confirmation, refuse synthesis here so
|
|
290
|
+
// the operator sees the credential-leak error immediately (and so the
|
|
291
|
+
// throw is not masked by downstream asset bundling). The resolved value
|
|
292
|
+
// is threaded into `defaultRouteSettings.dataTraceEnabled` below.
|
|
293
|
+
const dataTraceEnabled = resolveDataTraceEnabled(props);
|
|
168
294
|
|
|
169
295
|
// Server identity is exposed as CloudFormation Parameters so it can be
|
|
170
296
|
// overridden at deploy time (`cdk deploy --parameters ServerName=...`,
|
|
@@ -209,6 +335,34 @@ export class IrcAwsStack extends Stack {
|
|
|
209
335
|
default: 'INFO',
|
|
210
336
|
allowedValues: ['OFF', 'INFO', 'ERROR'],
|
|
211
337
|
});
|
|
338
|
+
|
|
339
|
+
// Stage-level throttling: the global backstop BEHIND the per-IP
|
|
340
|
+
// admission gates (the `sourceIp` GSI counts in the `$connect`
|
|
341
|
+
// handler and the per-IP rate knobs). Caps the aggregate
|
|
342
|
+
// requests/second (and burst) APIGW forwards to ANY route — a
|
|
343
|
+
// connect flood from many IPs hits this even when each single IP
|
|
344
|
+
// stays under its per-IP budget. Defaults (100 rps / burst 200) are
|
|
345
|
+
// generous for an IRC deployment sized by `maxClients`; tune at
|
|
346
|
+
// deploy time via `--parameters ApiThrottlingRateLimit=...`. For
|
|
347
|
+
// per-IP rate limiting at the EDGE, deploy a WAFv2 rate-based rule
|
|
348
|
+
// scoped to the `$connect` route (operator-side; see
|
|
349
|
+
// docs/AWS-Deployment.md § throttling).
|
|
350
|
+
const apiThrottlingRateParam = new CfnParameter(this, 'ApiThrottlingRateLimit', {
|
|
351
|
+
type: 'Number',
|
|
352
|
+
description:
|
|
353
|
+
'API Gateway stage throttling: max requests per second across all routes ' +
|
|
354
|
+
'(global backstop behind the per-IP admission gates). ' +
|
|
355
|
+
'Override at deploy time via `--parameters ApiThrottlingRateLimit=...`.',
|
|
356
|
+
default: 100,
|
|
357
|
+
});
|
|
358
|
+
const apiThrottlingBurstParam = new CfnParameter(this, 'ApiThrottlingBurstLimit', {
|
|
359
|
+
type: 'Number',
|
|
360
|
+
description:
|
|
361
|
+
'API Gateway stage throttling: burst capacity (concurrent requests allowed momentarily) ' +
|
|
362
|
+
'paired with ApiThrottlingRateLimit. ' +
|
|
363
|
+
'Override at deploy time via `--parameters ApiThrottlingBurstLimit=...`.',
|
|
364
|
+
default: 200,
|
|
365
|
+
});
|
|
212
366
|
const motd = (props.motdLines ?? DEFAULT_MOTD_LINES).join('\n');
|
|
213
367
|
|
|
214
368
|
// EventBridge schedule expressions are surfaced as CloudFormation
|
|
@@ -238,17 +392,66 @@ export class IrcAwsStack extends Stack {
|
|
|
238
392
|
default: Schedule.rate(PING_CHECKER_SCHEDULE_RATE).expressionString,
|
|
239
393
|
});
|
|
240
394
|
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
395
|
+
// DynamoDB table names are the bare logical ids (`Connections`,
|
|
396
|
+
// `Nicks`, …). There is a single deploy target per platform (the
|
|
397
|
+
// staging/prod split was collapsed); staging vs production isolation
|
|
398
|
+
// is driven by which AWS account + region the deploy credentials
|
|
399
|
+
// target, not by table-name prefixing.
|
|
400
|
+
//
|
|
401
|
+
// State-bearing tables (every table EXCEPT `Connections`) are protected
|
|
402
|
+
// from accidental data loss by THREE independent mechanisms, each
|
|
403
|
+
// toggleable so a deliberate teardown can still proceed:
|
|
404
|
+
// • `RemovalPolicy.RETAIN` — orphans the table (keeps its data) when
|
|
405
|
+
// the stack is deleted, instead of CloudFormation deleting it.
|
|
406
|
+
// • `deletionProtection: true` — DynamoDB refuses a `DeleteTable`
|
|
407
|
+
// API call while this is on, blocking a stray
|
|
408
|
+
// `aws dynamodb delete-table` even from an admin credential.
|
|
409
|
+
// • `pointInTimeRecovery` — continuous, per-table restore (within
|
|
410
|
+
// ~35 days) against accidental writes/deletes that RETAIN +
|
|
411
|
+
// deletion protection cannot stop (e.g. a buggy `UpdateItem`).
|
|
412
|
+
//
|
|
413
|
+
// NOTE: SNAPSHOT semantics are deliberately NOT used. DynamoDB tables
|
|
414
|
+
// do NOT snapshot on stack delete (`RemovalPolicy.SNAPSHOT` is a no-op
|
|
415
|
+
// here and would lull an operator into a false sense of backup). The
|
|
416
|
+
// correct combination is RETAIN (orphan + keep) + PITR (continuous,
|
|
417
|
+
// in-place restore). `Connections` is the sole exception: its rows are
|
|
418
|
+
// ephemeral per-connection records (TTL-reaped, sweeper-cleaned), so it
|
|
419
|
+
// stays DESTROY with no PITR and no deletion protection at all times.
|
|
420
|
+
//
|
|
421
|
+
// Cost: RETAIN and deletionProtection are free; PITR is billed
|
|
422
|
+
// continuously at ~$0.20/GB-month per table (see docs/AWS-Deployment.md).
|
|
423
|
+
const allowTableDeletion = props.allowTableDeletion === true;
|
|
424
|
+
const enablePitr = props.enablePitr !== false;
|
|
245
425
|
const tables = Object.fromEntries(
|
|
246
|
-
Object.entries(TABLE_DEFS).map(([logicalId,
|
|
426
|
+
Object.entries(TABLE_DEFS).map(([logicalId, tableDef]) => {
|
|
427
|
+
const isStateTable = logicalId !== EPHEMERAL_TABLE_LOGICAL_ID;
|
|
428
|
+
// `exactOptionalPropertyTypes` forbids passing `undefined` for
|
|
429
|
+
// optional props, so the protection knobs are spread in
|
|
430
|
+
// conditionally: Connections omits all three (always DESTROY,
|
|
431
|
+
// no PITR, no deletion protection); state tables set all three
|
|
432
|
+
// based on the flags.
|
|
433
|
+
const protectionProps: Partial<
|
|
434
|
+
Pick<TableProps, 'deletionProtection' | 'pointInTimeRecovery'>
|
|
435
|
+
> = isStateTable
|
|
436
|
+
? {
|
|
437
|
+
deletionProtection: !allowTableDeletion,
|
|
438
|
+
pointInTimeRecovery: enablePitr,
|
|
439
|
+
}
|
|
440
|
+
: {};
|
|
441
|
+
// The GSI side-channel rides alongside the construct props in
|
|
442
|
+
// TABLE_DEFS; pop it before constructing (current CDK declares
|
|
443
|
+
// indexes via addGlobalSecondaryIndex, not TableProps).
|
|
444
|
+
const { globalSecondaryIndexes, ...tableProps } = tableDef;
|
|
247
445
|
const table = new Table(this, logicalId, {
|
|
248
446
|
...tableProps,
|
|
249
|
-
tableName:
|
|
250
|
-
removalPolicy:
|
|
447
|
+
tableName: logicalId,
|
|
448
|
+
removalPolicy:
|
|
449
|
+
isStateTable && !allowTableDeletion ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
|
|
450
|
+
...protectionProps,
|
|
251
451
|
});
|
|
452
|
+
for (const gsi of globalSecondaryIndexes ?? []) {
|
|
453
|
+
table.addGlobalSecondaryIndex(gsi);
|
|
454
|
+
}
|
|
252
455
|
return [logicalId, table];
|
|
253
456
|
}),
|
|
254
457
|
);
|
|
@@ -352,19 +555,25 @@ export class IrcAwsStack extends Stack {
|
|
|
352
555
|
// APIGW validates this role when it is set as the account CloudWatch
|
|
353
556
|
// role, and rejects one scoped to a single log-group ARN: execution
|
|
354
557
|
// logs land in APIGW's OWN log groups (AWS/ApiGateway...), separate
|
|
355
|
-
// from this stack's access-log group. Grant the
|
|
356
|
-
// across all log groups
|
|
357
|
-
//
|
|
358
|
-
//
|
|
558
|
+
// from this stack's access-log group. Grant ONLY the three write
|
|
559
|
+
// actions APIGW needs, across all log groups (the wildcard is an
|
|
560
|
+
// inherent APIGW constraint — those group ARNs are not predictable at
|
|
561
|
+
// synth time). The legacy policy also carried Describe* actions; they
|
|
562
|
+
// were dropped so a compromised role cannot enumerate (or, given the
|
|
563
|
+
// read actions AWS's own managed policy for this purpose includes,
|
|
564
|
+
// exfiltrate) arbitrary log groups in the account.
|
|
565
|
+
//
|
|
566
|
+
// Managed-policy decision: the AWS-managed
|
|
567
|
+
// AmazonAPIGatewayPushToCloudWatchLogs policy was considered and
|
|
568
|
+
// REJECTED. Its fixed grant set is broader than needed — it grants
|
|
569
|
+
// DescribeLogGroups/DescribeLogStreams (deliberately dropped here)
|
|
570
|
+
// plus GetLogEvents/FilterLogEvents, all on '*' — and AWS can widen
|
|
571
|
+
// its contents without this stack being re-reviewed. It is also not
|
|
572
|
+
// available in every partition. The inline policy keeps the grant set
|
|
573
|
+
// exact and auditable at synth time.
|
|
359
574
|
apiLoggingRole.addToPolicy(
|
|
360
575
|
new PolicyStatement({
|
|
361
|
-
actions: [
|
|
362
|
-
'logs:CreateLogGroup',
|
|
363
|
-
'logs:CreateLogStream',
|
|
364
|
-
'logs:DescribeLogGroups',
|
|
365
|
-
'logs:DescribeLogStreams',
|
|
366
|
-
'logs:PutLogEvents',
|
|
367
|
-
],
|
|
576
|
+
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
|
|
368
577
|
resources: ['*'],
|
|
369
578
|
}),
|
|
370
579
|
);
|
|
@@ -414,13 +623,23 @@ export class IrcAwsStack extends Stack {
|
|
|
414
623
|
sourceIp: '$context.sourceIp',
|
|
415
624
|
}),
|
|
416
625
|
};
|
|
417
|
-
// Execution logging: the parameter drives LoggingLevel (OFF/INFO/ERROR)
|
|
418
|
-
// DataTraceEnabled
|
|
419
|
-
//
|
|
420
|
-
//
|
|
626
|
+
// Execution logging: the parameter drives LoggingLevel (OFF/INFO/ERROR).
|
|
627
|
+
// `DataTraceEnabled` is hard-locked to `false` here. For a WebSocket IRC
|
|
628
|
+
// endpoint, body tracing writes the FULL payload of every frame —
|
|
629
|
+
// `PASS <password>`, `AUTHENTICATE <base64-SASL-PLAIN>`,
|
|
630
|
+
// `JOIN #chan <channel-key>`, and every `PRIVMSG`/`NOTICE` — to the
|
|
631
|
+
// APIGW execution-log CloudWatch group (AWS/ApiGateway…). Anyone with
|
|
632
|
+
// `logs:GetLogEvents` recovers IRC credentials verbatim. The logging
|
|
633
|
+
// LEVEL (INFO request/response metadata without bodies) is enough to
|
|
634
|
+
// diagnose frame-routing drops; the only way to re-enable full-body
|
|
635
|
+
// tracing is the explicit, double-opt-in escape hatch asserted below.
|
|
421
636
|
cfnStage.defaultRouteSettings = {
|
|
422
637
|
loggingLevel: apiLoggingLevelParam.valueAsString,
|
|
423
|
-
dataTraceEnabled
|
|
638
|
+
dataTraceEnabled,
|
|
639
|
+
// Stage throttling (see the parameter definitions above): global
|
|
640
|
+
// rate + burst backstop behind the per-IP admission gates.
|
|
641
|
+
throttlingRateLimit: apiThrottlingRateParam.valueAsNumber,
|
|
642
|
+
throttlingBurstLimit: apiThrottlingBurstParam.valueAsNumber,
|
|
424
643
|
};
|
|
425
644
|
|
|
426
645
|
stage.grantManagementApiAccess(handler);
|
|
@@ -428,18 +647,64 @@ export class IrcAwsStack extends Stack {
|
|
|
428
647
|
handler.addEnvironment('MANAGEMENT_URL', stage.callbackUrl);
|
|
429
648
|
pingChecker.addEnvironment('MANAGEMENT_URL', stage.callbackUrl);
|
|
430
649
|
|
|
650
|
+
// Optional WAFv2 rate-based rule on the WebSocket API's `$connect`
|
|
651
|
+
// (see the `wafConnectRateLimit` prop doc): WAF only inspects the
|
|
652
|
+
// initial HTTP upgrade on a WebSocket API, so the rate statement
|
|
653
|
+
// applies exactly where credential brute-force has to spend its
|
|
654
|
+
// budget — connection establishment — and never to established
|
|
655
|
+
// frames. Blocks happen at the edge, before a Lambda invocation is
|
|
656
|
+
// billed. Unset → no WAF resources at all (the default).
|
|
657
|
+
if (props.wafConnectRateLimit !== undefined) {
|
|
658
|
+
const webAcl = new CfnWebACL(this, 'ConnectRateWebAcl', {
|
|
659
|
+
scope: 'REGIONAL',
|
|
660
|
+
defaultAction: { allow: {} },
|
|
661
|
+
// CFN requires a VisibilityConfig on both the ACL and the rule;
|
|
662
|
+
// sampled requests + the CloudWatch metric keep operator
|
|
663
|
+
// visibility without full request logging.
|
|
664
|
+
visibilityConfig: {
|
|
665
|
+
cloudWatchMetricsEnabled: true,
|
|
666
|
+
metricName: 'IrcConnectWebAcl',
|
|
667
|
+
sampledRequestsEnabled: true,
|
|
668
|
+
},
|
|
669
|
+
rules: [
|
|
670
|
+
{
|
|
671
|
+
name: 'ConnectRateLimit',
|
|
672
|
+
priority: 0,
|
|
673
|
+
action: { block: {} },
|
|
674
|
+
visibilityConfig: {
|
|
675
|
+
cloudWatchMetricsEnabled: true,
|
|
676
|
+
metricName: 'IrcConnectRateLimit',
|
|
677
|
+
sampledRequestsEnabled: true,
|
|
678
|
+
},
|
|
679
|
+
statement: {
|
|
680
|
+
rateBasedStatement: {
|
|
681
|
+
limit: props.wafConnectRateLimit,
|
|
682
|
+
aggregateKeyType: 'IP',
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
});
|
|
688
|
+
new CfnWebACLAssociation(this, 'ConnectRateWebAclAssociation', {
|
|
689
|
+
webAclArn: webAcl.attrArn,
|
|
690
|
+
// WAF associates with API Gateway stages via the v1-style stage
|
|
691
|
+
// ARN namespace (`/restapis/…`) — WebSocket APIs use the same
|
|
692
|
+
// format (there is no `WebSocketStage.stageArn` L2 helper).
|
|
693
|
+
resourceArn: `arn:${this.partition}:apigateway:${this.region}::/restapis/${webSocketApi.apiId}/stages/${stage.stageName}`,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
431
697
|
// Inject each table's physical name as an env var so every Lambda can
|
|
432
698
|
// construct a `TablesConfig` at cold start via `buildDepsFromEnv`. The
|
|
433
|
-
// env-var value is the
|
|
434
|
-
// through the same
|
|
435
|
-
// exactly), NOT a CloudFormation Ref — the runtime needs the
|
|
436
|
-
//
|
|
699
|
+
// env-var value is the literal physical table name (the bare logical
|
|
700
|
+
// id, computed through the same mapping as the Table construct so they
|
|
701
|
+
// match exactly), NOT a CloudFormation Ref — the runtime needs the
|
|
702
|
+
// literal name to target the correct table.
|
|
437
703
|
for (const logicalId of Object.keys(TABLE_DEFS)) {
|
|
438
704
|
const envName = `${logicalId.toUpperCase()}_TABLE`;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
pingChecker.addEnvironment(envName, physicalName);
|
|
705
|
+
handler.addEnvironment(envName, logicalId);
|
|
706
|
+
sweeper.addEnvironment(envName, logicalId);
|
|
707
|
+
pingChecker.addEnvironment(envName, logicalId);
|
|
443
708
|
}
|
|
444
709
|
|
|
445
710
|
// Server identity is injected into the handler, ping checker, AND the
|
|
@@ -461,6 +726,16 @@ export class IrcAwsStack extends Stack {
|
|
|
461
726
|
sweeper.addEnvironment('NETWORK_NAME', networkName);
|
|
462
727
|
sweeper.addEnvironment('MOTD', motd);
|
|
463
728
|
|
|
729
|
+
// CSWSH defence — opt-in `WEB_ORIGINS` allowlist on the `$connect`
|
|
730
|
+
// handler. Only the wss handler runs `$connect`, so only it needs
|
|
731
|
+
// the env var (the sweeper, ping-checker, and NLB handler never see
|
|
732
|
+
// an `event.headers.Origin`). When the prop is unset/empty the env
|
|
733
|
+
// var is omitted entirely and the handler's defence is disabled —
|
|
734
|
+
// existing deployments without a web frontend are unchanged.
|
|
735
|
+
if (props.webOrigins !== undefined && props.webOrigins.length > 0) {
|
|
736
|
+
handler.addEnvironment('WEB_ORIGINS', props.webOrigins);
|
|
737
|
+
}
|
|
738
|
+
|
|
464
739
|
// EventBridge schedule → sweeper Lambda. The schedule expression comes
|
|
465
740
|
// from a CloudFormation Parameter (see above), so it can be overridden
|
|
466
741
|
// at deploy time; the sweeper is self-throttling
|
|
@@ -526,12 +801,18 @@ export class IrcAwsStack extends Stack {
|
|
|
526
801
|
// Inject the same table names, server identity, and management URL.
|
|
527
802
|
for (const logicalId of Object.keys(TABLE_DEFS)) {
|
|
528
803
|
const envName = `${logicalId.toUpperCase()}_TABLE`;
|
|
529
|
-
nlbHandler.addEnvironment(envName,
|
|
804
|
+
nlbHandler.addEnvironment(envName, logicalId);
|
|
530
805
|
}
|
|
531
806
|
nlbHandler.addEnvironment('SERVER_NAME', serverName);
|
|
532
807
|
nlbHandler.addEnvironment('NETWORK_NAME', networkName);
|
|
533
808
|
nlbHandler.addEnvironment('MOTD', motd);
|
|
534
809
|
nlbHandler.addEnvironment('MANAGEMENT_URL', stage.callbackUrl);
|
|
810
|
+
// The NLB handler fans channel messages out to OTHER connections via
|
|
811
|
+
// the management API — including wss clients on the API Gateway
|
|
812
|
+
// transport — so it needs the same `execute-api:ManageConnections`
|
|
813
|
+
// grant as the wss handler and ping checker. Without it every
|
|
814
|
+
// cross-transport `postToConnection` fails with 403.
|
|
815
|
+
stage.grantManagementApiAccess(nlbHandler);
|
|
535
816
|
|
|
536
817
|
const nlb = new NetworkLoadBalancer(this, 'IrcNlb', {
|
|
537
818
|
vpc,
|
|
@@ -571,6 +852,39 @@ export class IrcAwsStack extends Stack {
|
|
|
571
852
|
description: 'HTTPS management endpoint for ApiGatewayManagementApi.postToConnection.',
|
|
572
853
|
value: stage.callbackUrl,
|
|
573
854
|
});
|
|
855
|
+
|
|
856
|
+
// -------------------------------------------------------------------------
|
|
857
|
+
// Web client hosting substrate (S3 + CloudFront + OAC).
|
|
858
|
+
//
|
|
859
|
+
// Serves the shared `apps/web/dist` artifact (Kiwi SPA + landing page)
|
|
860
|
+
// exclusively over CloudFront. The S3 origin bucket is private; CloudFront
|
|
861
|
+
// reads it via Origin Access Control (the modern replacement for OAI).
|
|
862
|
+
// Only provisioned when `webSite` is supplied — staging defaults to the
|
|
863
|
+
// `*.cloudfront.net` URL, prod passes the custom-domain fields. Pure
|
|
864
|
+
// infrastructure; the per-env config bake and the s3-sync deploy pipeline
|
|
865
|
+
// are separate.
|
|
866
|
+
// -------------------------------------------------------------------------
|
|
867
|
+
if (props.webSite !== undefined) {
|
|
868
|
+
const site = new StaticSite(this, 'StaticSite', props.webSite);
|
|
869
|
+
// Expose the deploy-pipeline-relevant resource IDs at the PARENT-stack
|
|
870
|
+
// level (stable logical IDs — no CDK hashing, which only kicks in for
|
|
871
|
+
// outputs nested under child constructs). `scripts/deploy-web-aws.mjs`
|
|
872
|
+
// reads these via `aws cloudformation describe-stacks --query` to drive
|
|
873
|
+
// `s3 sync apps/web/dist s3://<bucket> --delete` and
|
|
874
|
+
// `cloudfront create-invalidation --distribution-id <id> --paths /*`.
|
|
875
|
+
// `ConnectUrl` (above) supplies the wss URL that the same script bakes
|
|
876
|
+
// into the SPA's config.json via `build.mjs --api-url`.
|
|
877
|
+
new CfnOutput(this, 'WebsiteBucketName', {
|
|
878
|
+
description:
|
|
879
|
+
'S3 origin bucket holding the web client build artifact (apps/web/dist). The deploy pipeline runs `aws s3 sync apps/web/dist s3://<this-bucket> --delete`.',
|
|
880
|
+
value: site.bucket.bucketName,
|
|
881
|
+
});
|
|
882
|
+
new CfnOutput(this, 'WebsiteDistributionId', {
|
|
883
|
+
description:
|
|
884
|
+
'CloudFront distribution ID serving the web client. The deploy pipeline runs `aws cloudfront create-invalidation --distribution-id <this-id> --paths /*` after each s3 sync.',
|
|
885
|
+
value: site.distribution.distributionId,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
574
888
|
}
|
|
575
889
|
}
|
|
576
890
|
|
|
@@ -582,6 +896,34 @@ function handlerEntry(): string {
|
|
|
582
896
|
return resolve(HERE, '..', '..', '..', 'packages', 'aws-adapter', 'src', 'handlers', 'index.ts');
|
|
583
897
|
}
|
|
584
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Resolves the API Gateway `DataTraceEnabled` flag for the stage from the
|
|
901
|
+
* construct props. Safe-by-default: returns `false` unless the explicit,
|
|
902
|
+
* documented escape hatch is invoked. See {@link IrcStackProps.allowDataTrace}.
|
|
903
|
+
*
|
|
904
|
+
* Post-T170 (staging/prod collapse) every synth is effectively a prod synth,
|
|
905
|
+
* so there is no "non-prod" branch to quietly enable tracing on. The guard
|
|
906
|
+
* therefore refuses synthesis the moment `allowDataTrace` is requested
|
|
907
|
+
* without the explicit `iUnderstandThisLeaksCredentials` acknowledgement —
|
|
908
|
+
* forcing the operator to read the credential-leak warning.
|
|
909
|
+
*/
|
|
910
|
+
function resolveDataTraceEnabled(props: IrcStackProps): boolean {
|
|
911
|
+
if (props.allowDataTrace === true) {
|
|
912
|
+
if (props.iUnderstandThisLeaksCredentials === true) {
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
throw new Error(
|
|
916
|
+
'Refusing to enable API Gateway DataTraceEnabled: for a WebSocket IRC ' +
|
|
917
|
+
'endpoint this logs the FULL body of every frame (PASS <password>, ' +
|
|
918
|
+
'AUTHENTICATE <base64-SASL-PLAIN>, JOIN #chan <channel-key>, every ' +
|
|
919
|
+
'PRIVMSG/NOTICE) to the APIGW CloudWatch log group, leaking IRC ' +
|
|
920
|
+
'credentials to anyone with logs:GetLogEvents. To override in a ' +
|
|
921
|
+
'throwaway sandbox ONLY, also pass iUnderstandThisLeaksCredentials=true.',
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
|
|
585
927
|
/**
|
|
586
928
|
* Internal Lambda-construct props: `NodejsFunctionProps` narrowed so the
|
|
587
929
|
* runtime and handler are required. Every call site in the stack already
|
|
@@ -623,14 +965,3 @@ function makeLambda(scope: Construct, id: string, props: StrictLambdaProps): Lam
|
|
|
623
965
|
}
|
|
624
966
|
return new NodejsFunction(scope, id, props);
|
|
625
967
|
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Builds the physical DynamoDB table name for a logical id under the
|
|
629
|
-
* given environment. Capitalises the environment's first letter so it
|
|
630
|
-
* composes cleanly with PascalCase logical ids
|
|
631
|
-
* (`staging` + `Connections` → `StagingConnections`).
|
|
632
|
-
*/
|
|
633
|
-
function prefixedTableName(environmentName: string, logicalId: string): string {
|
|
634
|
-
const cap = environmentName.charAt(0).toUpperCase() + environmentName.slice(1);
|
|
635
|
-
return `${cap}${logicalId}`;
|
|
636
|
-
}
|