serverless-ircd 0.9.0 → 0.10.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/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +11 -9
- package/.github/workflows/deploy-cf.yml +14 -14
- package/CHANGELOG.md +305 -0
- package/README.md +134 -41
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +82 -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 +177 -52
- 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 +267 -92
- 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/package.json +2 -3
- package/apps/cf-tcp-container/src/container-server.ts +12 -9
- package/apps/cf-tcp-container/wrangler.toml +1 -10
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/wrangler.toml +12 -71
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +21 -17
- 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 +670 -96
- package/docs/AWS-TCP-Deployment.md +20 -45
- package/docs/Cloudflare-Deployment-Guide.md +87 -113
- package/docs/Cloudflare-TCP-Deployment.md +25 -49
- package/docs/Release-Process.md +27 -23
- 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/cdk-table-defs.ts +6 -11
- package/packages/aws-adapter/src/config-loader.ts +19 -2
- package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
- package/packages/aws-adapter/src/handlers/connect.ts +26 -0
- package/packages/aws-adapter/src/handlers/default.ts +190 -123
- package/packages/aws-adapter/src/handlers/index.ts +67 -23
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +3 -6
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +15 -0
- package/packages/aws-adapter/src/tables.ts +2 -12
- package/packages/aws-adapter/tests/aws-harness.ts +0 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
- package/packages/aws-adapter/tests/connect.test.ts +124 -1
- package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -0
- package/packages/aws-adapter/tests/handlers.test.ts +117 -11
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- 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 +59 -8
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/connection-do.ts +266 -109
- package/packages/cf-adapter/src/d1-services-store.ts +63 -26
- package/packages/cf-adapter/src/env.ts +11 -10
- package/packages/cf-adapter/src/index.ts +0 -6
- package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +95 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +122 -11
- package/packages/irc-core/src/commands/registration.ts +2 -2
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- package/packages/irc-core/src/config.ts +35 -9
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +159 -179
- package/packages/irc-core/src/types.ts +38 -10
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- 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/nickserv.test.ts +237 -1
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +140 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +159 -178
- package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
- package/packages/irc-core/tests/ports.test.ts +71 -0
- package/packages/irc-core/tests/services-store.test.ts +204 -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 +0 -14
- package/packages/irc-server/tests/actor.test.ts +19 -47
- package/packages/irc-test-support/package.json +1 -1
- 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 +1 -1
- package/tools/ci-hardening/src/index.ts +2 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -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
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* CDK CLI's default account/region when those env vars are set; otherwise
|
|
6
6
|
* the stack is environment-agnostic (suitable for `cdk synth` and localstack).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* There is a single deploy target per platform (the staging/prod split was
|
|
9
|
+
* collapsed). This app synthesizes exactly one stack named `IrcAwsStack`;
|
|
10
|
+
* staging vs production isolation is driven by which AWS account and region
|
|
11
|
+
* the deploy credentials target, not by stack-name templating. Server
|
|
12
|
+
* identity (`serverName`, `networkName`, `motd`) and the optional TCP+TLS
|
|
13
|
+
* domain are read from CDK context so deploys can override them without
|
|
14
|
+
* editing source.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import { App } from 'aws-cdk-lib';
|
|
@@ -20,8 +22,6 @@ const app = new App();
|
|
|
20
22
|
|
|
21
23
|
const account = process.env.CDK_DEFAULT_ACCOUNT;
|
|
22
24
|
const region = process.env.CDK_DEFAULT_REGION;
|
|
23
|
-
const environmentName =
|
|
24
|
-
(app.node.tryGetContext('environmentName') as string | undefined) ?? 'staging';
|
|
25
25
|
|
|
26
26
|
// Server identity is read from CDK context so deploys can override it
|
|
27
27
|
// without editing source (`-c serverName=…`, `-c networkName=…`,
|
|
@@ -44,13 +44,86 @@ const motdLines = Array.isArray(rawMotd)
|
|
|
44
44
|
// docs/AWS-TCP-Deployment.md.
|
|
45
45
|
const tcpTlsDomainName = app.node.tryGetContext('tcpTlsDomainName') as string | undefined;
|
|
46
46
|
|
|
47
|
+
// `$connect` Origin allowlist (CSWSH defence). Opt-in: only provisioned
|
|
48
|
+
// when this context var is supplied as a non-empty string. Comma-separated
|
|
49
|
+
// origin list (e.g. `'https://app.example.com, https://staging.app.example.com'`).
|
|
50
|
+
// When omitted, the `$connect` handler skips the Origin check entirely
|
|
51
|
+
// (existing AWS deployments without a web frontend are unchanged).
|
|
52
|
+
const webOrigins = app.node.tryGetContext('webOrigins') as string | undefined;
|
|
53
|
+
|
|
54
|
+
// API Gateway full-frame body tracing (`DataTraceEnabled`). Safe-by-default:
|
|
55
|
+
// omitted → `false` (no IRC frame bodies logged). Re-enabling requires BOTH
|
|
56
|
+
// `-c allowDataTrace=true` AND `-c iUnderstandThisLeaksCredentials=true`; the
|
|
57
|
+
// stack REFUSES synth with a credential-leak error if only the first is set.
|
|
58
|
+
// Since the staging/prod split was collapsed every synth is effectively a
|
|
59
|
+
// prod synth, so there is no quiet "non-prod" path — use ONLY in a throwaway
|
|
60
|
+
// sandbox for live debugging. See docs/AWS-Deployment.md (§7.6).
|
|
61
|
+
const allowDataTrace = app.node.tryGetContext('allowDataTrace') as boolean | string | undefined;
|
|
62
|
+
const iUnderstandThisLeaksCredentials = app.node.tryGetContext('iUnderstandThisLeaksCredentials') as
|
|
63
|
+
| boolean
|
|
64
|
+
| string
|
|
65
|
+
| undefined;
|
|
66
|
+
|
|
67
|
+
// CDK context vars arrive as strings; normalise the booleans so the stack's
|
|
68
|
+
// guard sees real booleans (and `--c allowDataTrace=false` stays false-y).
|
|
69
|
+
const allowDataTraceBool = parseContextBool(allowDataTrace);
|
|
70
|
+
const acknowledgeLeakBool = parseContextBool(iUnderstandThisLeaksCredentials);
|
|
71
|
+
|
|
72
|
+
// Web client hosting (S3 + CloudFront + OAC) is opt-in. Supplying
|
|
73
|
+
// `webSiteCustomDomain` provisions the static-site construct; prod passes
|
|
74
|
+
// the custom domain + cert + hosted zone, staging leaves them unset to use
|
|
75
|
+
// the default `*.cloudfront.net` URL. See docs/AWS-Deployment.md (web section).
|
|
76
|
+
const webSiteCustomDomain = app.node.tryGetContext('webSiteCustomDomain') as string | undefined;
|
|
77
|
+
const webSiteCertificateArn = app.node.tryGetContext('webSiteCertificateArn') as string | undefined;
|
|
78
|
+
const webSiteHostedZoneName = app.node.tryGetContext('webSiteHostedZoneName') as string | undefined;
|
|
79
|
+
const webSiteHostedZoneId = app.node.tryGetContext('webSiteHostedZoneId') as string | undefined;
|
|
80
|
+
const webSite =
|
|
81
|
+
webSiteCustomDomain !== undefined
|
|
82
|
+
? {
|
|
83
|
+
customDomain: webSiteCustomDomain,
|
|
84
|
+
...(webSiteCertificateArn !== undefined ? { certificateArn: webSiteCertificateArn } : {}),
|
|
85
|
+
...(webSiteHostedZoneName !== undefined ? { hostedZoneName: webSiteHostedZoneName } : {}),
|
|
86
|
+
...(webSiteHostedZoneId !== undefined ? { hostedZoneId: webSiteHostedZoneId } : {}),
|
|
87
|
+
}
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
47
90
|
const stackProps: IrcStackProps = {
|
|
48
|
-
environmentName,
|
|
49
91
|
...(serverName !== undefined ? { serverName } : {}),
|
|
50
92
|
...(networkName !== undefined ? { networkName } : {}),
|
|
51
93
|
...(motdLines !== undefined ? { motdLines } : {}),
|
|
52
94
|
...(tcpTlsDomainName !== undefined ? { tcpTlsDomainName } : {}),
|
|
95
|
+
...(webOrigins !== undefined && webOrigins.length > 0 ? { webOrigins } : {}),
|
|
96
|
+
...(webSite !== undefined ? { webSite } : {}),
|
|
97
|
+
...(allowDataTraceBool !== undefined ? { allowDataTrace: allowDataTraceBool } : {}),
|
|
98
|
+
...(acknowledgeLeakBool !== undefined
|
|
99
|
+
? { iUnderstandThisLeaksCredentials: acknowledgeLeakBool }
|
|
100
|
+
: {}),
|
|
53
101
|
...(account && region ? { env: { account, region } } : {}),
|
|
54
102
|
};
|
|
55
103
|
|
|
56
|
-
new IrcAwsStack(app,
|
|
104
|
+
new IrcAwsStack(app, 'IrcAwsStack', stackProps);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Normalises a CDK context value (`-c key=value`) into a boolean.
|
|
108
|
+
* CDK passes context values as strings, so `'true'` / `'false'` are
|
|
109
|
+
* parsed; anything else (including `undefined`) returns `undefined` so
|
|
110
|
+
* the downstream optional prop is simply omitted. The data-trace guard
|
|
111
|
+
* in `IrcAwsStack` is strict-equality-checked against `true`, so a stray
|
|
112
|
+
* string here would otherwise defeat the opt-in.
|
|
113
|
+
*/
|
|
114
|
+
function parseContextBool(value: boolean | string | undefined): boolean | undefined {
|
|
115
|
+
if (value === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === 'boolean') {
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
const normalised = value.trim().toLowerCase();
|
|
122
|
+
if (normalised === 'true') {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (normalised === 'false') {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
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.10.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`).
|
|
@@ -51,6 +51,7 @@ 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
53
|
import type { Construct } from 'constructs';
|
|
54
|
+
import { StaticSite, type StaticSiteProps } from './static-site.js';
|
|
54
55
|
|
|
55
56
|
/** Stage name served by the auto-deploying WebSocket stage. */
|
|
56
57
|
const STAGE_NAME = 'prod';
|
|
@@ -68,13 +69,6 @@ const SWEEPER_SCHEDULE_RATE = Duration.minutes(5);
|
|
|
68
69
|
*/
|
|
69
70
|
const PING_CHECKER_SCHEDULE_RATE = Duration.minutes(1);
|
|
70
71
|
|
|
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
72
|
/**
|
|
79
73
|
* Fallback server identity, used when the corresponding prop is omitted.
|
|
80
74
|
* Deliberately not `irc.example.com` (the retired placeholder) so a
|
|
@@ -109,21 +103,10 @@ const TEST_NO_BUNDLE_FLAG = 'IRC_AWS_STACK_TEST_NO_BUNDLE';
|
|
|
109
103
|
|
|
110
104
|
/**
|
|
111
105
|
* Construct props for {@link IrcAwsStack}. Acts as the single
|
|
112
|
-
* configuration bag extended by deploy-time knobs (
|
|
113
|
-
*
|
|
106
|
+
* configuration bag extended by deploy-time knobs (server identity, …)
|
|
107
|
+
* so the prop surface grows in one place.
|
|
114
108
|
*/
|
|
115
109
|
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
110
|
/**
|
|
128
111
|
* Server name advertised to clients (`SERVER_NAME` / the `001`
|
|
129
112
|
* welcome source). Falls back to `irc.localhost` when omitted; production
|
|
@@ -158,13 +141,89 @@ export interface IrcStackProps extends StackProps {
|
|
|
158
141
|
* (WebSocket) transport is deployed.
|
|
159
142
|
*/
|
|
160
143
|
readonly tcpTlsDomainName?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Web client hosting substrate (S3 + CloudFront + OAC).
|
|
147
|
+
*
|
|
148
|
+
* When provided, the stack provisions a {@link StaticSite} that serves
|
|
149
|
+
* the shared `apps/web/dist` build artifact over CloudFront. The S3
|
|
150
|
+
* origin bucket is private and grants the distribution via Origin
|
|
151
|
+
* Access Control (never public-read); SPA client-side routing under
|
|
152
|
+
* `/app/*` is handled by a CloudFront custom error response mapping
|
|
153
|
+
* 403/404 → `200 /app/index.html`. Pure infrastructure — the per-env
|
|
154
|
+
* config bake and the s3-sync deploy pipeline are separate. Pass the
|
|
155
|
+
* custom-domain fields for prod; omit them for the default
|
|
156
|
+
* `*.cloudfront.net` URL (staging). When omitted entirely, no web
|
|
157
|
+
* hosting resources are provisioned.
|
|
158
|
+
*/
|
|
159
|
+
readonly webSite?: StaticSiteProps;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Comma-separated allowlist of web origins permitted to open
|
|
163
|
+
* WebSocket upgrades (CSWSH defence — API Gateway WebSocket has no
|
|
164
|
+
* built-in Origin check). Mirrors the CF Worker's `WEB_ORIGINS` env.
|
|
165
|
+
*
|
|
166
|
+
* When provided (non-empty), the value is injected as the
|
|
167
|
+
* `WEB_ORIGINS` Lambda env var on the wss `$connect` handler. The
|
|
168
|
+
* `$connect` Lambda then denies any browser-sent `Origin` not in
|
|
169
|
+
* the list with `403`, so API Gateway closes the upgrade. Unset /
|
|
170
|
+
* empty (the default) disables the check — existing deployments
|
|
171
|
+
* without a web frontend are unchanged on upgrade. Non-browser
|
|
172
|
+
* clients (curl, WeeChat, the `tcp-ws-forwarder`, NLB TCP+TLS)
|
|
173
|
+
* never send `Origin` and pass through regardless.
|
|
174
|
+
*
|
|
175
|
+
* Example: `'https://app.example.com, https://staging.app.example.com'`
|
|
176
|
+
*/
|
|
177
|
+
readonly webOrigins?: string;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Escape hatch that re-enables API Gateway full-frame body tracing
|
|
181
|
+
* (`DataTraceEnabled = true`) on the WebSocket stage. Defaults to
|
|
182
|
+
* `false` (safe: no frame bodies are logged).
|
|
183
|
+
*
|
|
184
|
+
* ⚠️ Setting this to `true` alone REFUSES synthesis — it must be
|
|
185
|
+
* accompanied by {@link iUnderstandThisLeaksCredentials} = `true`.
|
|
186
|
+
* This double opt-in is deliberate: since the staging/prod split was
|
|
187
|
+
* collapsed (every synth is effectively a prod synth; there is no
|
|
188
|
+
* `environmentName` context), there is no longer a "non-prod" branch
|
|
189
|
+
* to quietly enable tracing on. The only safe default is OFF, and the
|
|
190
|
+
* only way back on is an explicit, two-step acknowledgement.
|
|
191
|
+
*
|
|
192
|
+
* For a WebSocket IRC endpoint, body tracing writes the FULL payload
|
|
193
|
+
* of every frame — `PASS <password>`, `AUTHENTICATE <base64-SASL-PLAIN>`,
|
|
194
|
+
* `JOIN #chan <channel-key>`, and every `PRIVMSG`/`NOTICE` — to the
|
|
195
|
+
* APIGW execution-log CloudWatch group. Anyone with `logs:GetLogEvents`
|
|
196
|
+
* (operators, SREs, Logpush destinations, compromised CI creds)
|
|
197
|
+
* recovers IRC credentials verbatim. Use ONLY in a throwaway sandbox
|
|
198
|
+
* account for live debugging, and tear it down immediately after.
|
|
199
|
+
*
|
|
200
|
+
* Set via CDK context: `-c allowDataTrace=true`.
|
|
201
|
+
*/
|
|
202
|
+
readonly allowDataTrace?: boolean;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Mandatory confirmation that accompanies {@link allowDataTrace} = `true`.
|
|
206
|
+
* Has NO effect on its own; it only unlocks the `allowDataTrace` escape
|
|
207
|
+
* hatch. Defaults to `false`. The awkward name is intentional: it cannot
|
|
208
|
+
* be set by accident, and a synth error names this prop when it is
|
|
209
|
+
* missing so the operator is forced to read the credential-leak warning.
|
|
210
|
+
*
|
|
211
|
+
* Set via CDK context: `-c iUnderstandThisLeaksCredentials=true`.
|
|
212
|
+
*/
|
|
213
|
+
readonly iUnderstandThisLeaksCredentials?: boolean;
|
|
161
214
|
}
|
|
162
215
|
|
|
163
216
|
export class IrcAwsStack extends Stack {
|
|
164
217
|
constructor(scope: Construct, id: string, props: IrcStackProps = {}) {
|
|
165
218
|
super(scope, id, props);
|
|
166
219
|
|
|
167
|
-
|
|
220
|
+
// Fail-fast on the credential-leak escape hatch BEFORE any resource is
|
|
221
|
+
// constructed: if `allowDataTrace` was requested without the explicit
|
|
222
|
+
// `iUnderstandThisLeaksCredentials` confirmation, refuse synthesis here so
|
|
223
|
+
// the operator sees the credential-leak error immediately (and so the
|
|
224
|
+
// throw is not masked by downstream asset bundling). The resolved value
|
|
225
|
+
// is threaded into `defaultRouteSettings.dataTraceEnabled` below.
|
|
226
|
+
const dataTraceEnabled = resolveDataTraceEnabled(props);
|
|
168
227
|
|
|
169
228
|
// Server identity is exposed as CloudFormation Parameters so it can be
|
|
170
229
|
// overridden at deploy time (`cdk deploy --parameters ServerName=...`,
|
|
@@ -238,15 +297,16 @@ export class IrcAwsStack extends Stack {
|
|
|
238
297
|
default: Schedule.rate(PING_CHECKER_SCHEDULE_RATE).expressionString,
|
|
239
298
|
});
|
|
240
299
|
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
300
|
+
// DynamoDB table names are the bare logical ids (`Connections`,
|
|
301
|
+
// `Nicks`, …). There is a single deploy target per platform (the
|
|
302
|
+
// staging/prod split was collapsed); staging vs production isolation
|
|
303
|
+
// is driven by which AWS account + region the deploy credentials
|
|
304
|
+
// target, not by table-name prefixing.
|
|
245
305
|
const tables = Object.fromEntries(
|
|
246
306
|
Object.entries(TABLE_DEFS).map(([logicalId, tableProps]) => {
|
|
247
307
|
const table = new Table(this, logicalId, {
|
|
248
308
|
...tableProps,
|
|
249
|
-
tableName:
|
|
309
|
+
tableName: logicalId,
|
|
250
310
|
removalPolicy: RemovalPolicy.DESTROY,
|
|
251
311
|
});
|
|
252
312
|
return [logicalId, table];
|
|
@@ -414,13 +474,19 @@ export class IrcAwsStack extends Stack {
|
|
|
414
474
|
sourceIp: '$context.sourceIp',
|
|
415
475
|
}),
|
|
416
476
|
};
|
|
417
|
-
// Execution logging: the parameter drives LoggingLevel (OFF/INFO/ERROR)
|
|
418
|
-
// DataTraceEnabled
|
|
419
|
-
//
|
|
420
|
-
//
|
|
477
|
+
// Execution logging: the parameter drives LoggingLevel (OFF/INFO/ERROR).
|
|
478
|
+
// `DataTraceEnabled` is hard-locked to `false` here. For a WebSocket IRC
|
|
479
|
+
// endpoint, body tracing writes the FULL payload of every frame —
|
|
480
|
+
// `PASS <password>`, `AUTHENTICATE <base64-SASL-PLAIN>`,
|
|
481
|
+
// `JOIN #chan <channel-key>`, and every `PRIVMSG`/`NOTICE` — to the
|
|
482
|
+
// APIGW execution-log CloudWatch group (AWS/ApiGateway…). Anyone with
|
|
483
|
+
// `logs:GetLogEvents` recovers IRC credentials verbatim. The logging
|
|
484
|
+
// LEVEL (INFO request/response metadata without bodies) is enough to
|
|
485
|
+
// diagnose frame-routing drops; the only way to re-enable full-body
|
|
486
|
+
// tracing is the explicit, double-opt-in escape hatch asserted below.
|
|
421
487
|
cfnStage.defaultRouteSettings = {
|
|
422
488
|
loggingLevel: apiLoggingLevelParam.valueAsString,
|
|
423
|
-
dataTraceEnabled
|
|
489
|
+
dataTraceEnabled,
|
|
424
490
|
};
|
|
425
491
|
|
|
426
492
|
stage.grantManagementApiAccess(handler);
|
|
@@ -430,16 +496,15 @@ export class IrcAwsStack extends Stack {
|
|
|
430
496
|
|
|
431
497
|
// Inject each table's physical name as an env var so every Lambda can
|
|
432
498
|
// 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
|
-
//
|
|
499
|
+
// env-var value is the literal physical table name (the bare logical
|
|
500
|
+
// id, computed through the same mapping as the Table construct so they
|
|
501
|
+
// match exactly), NOT a CloudFormation Ref — the runtime needs the
|
|
502
|
+
// literal name to target the correct table.
|
|
437
503
|
for (const logicalId of Object.keys(TABLE_DEFS)) {
|
|
438
504
|
const envName = `${logicalId.toUpperCase()}_TABLE`;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
pingChecker.addEnvironment(envName, physicalName);
|
|
505
|
+
handler.addEnvironment(envName, logicalId);
|
|
506
|
+
sweeper.addEnvironment(envName, logicalId);
|
|
507
|
+
pingChecker.addEnvironment(envName, logicalId);
|
|
443
508
|
}
|
|
444
509
|
|
|
445
510
|
// Server identity is injected into the handler, ping checker, AND the
|
|
@@ -461,6 +526,16 @@ export class IrcAwsStack extends Stack {
|
|
|
461
526
|
sweeper.addEnvironment('NETWORK_NAME', networkName);
|
|
462
527
|
sweeper.addEnvironment('MOTD', motd);
|
|
463
528
|
|
|
529
|
+
// CSWSH defence — opt-in `WEB_ORIGINS` allowlist on the `$connect`
|
|
530
|
+
// handler. Only the wss handler runs `$connect`, so only it needs
|
|
531
|
+
// the env var (the sweeper, ping-checker, and NLB handler never see
|
|
532
|
+
// an `event.headers.Origin`). When the prop is unset/empty the env
|
|
533
|
+
// var is omitted entirely and the handler's defence is disabled —
|
|
534
|
+
// existing deployments without a web frontend are unchanged.
|
|
535
|
+
if (props.webOrigins !== undefined && props.webOrigins.length > 0) {
|
|
536
|
+
handler.addEnvironment('WEB_ORIGINS', props.webOrigins);
|
|
537
|
+
}
|
|
538
|
+
|
|
464
539
|
// EventBridge schedule → sweeper Lambda. The schedule expression comes
|
|
465
540
|
// from a CloudFormation Parameter (see above), so it can be overridden
|
|
466
541
|
// at deploy time; the sweeper is self-throttling
|
|
@@ -526,7 +601,7 @@ export class IrcAwsStack extends Stack {
|
|
|
526
601
|
// Inject the same table names, server identity, and management URL.
|
|
527
602
|
for (const logicalId of Object.keys(TABLE_DEFS)) {
|
|
528
603
|
const envName = `${logicalId.toUpperCase()}_TABLE`;
|
|
529
|
-
nlbHandler.addEnvironment(envName,
|
|
604
|
+
nlbHandler.addEnvironment(envName, logicalId);
|
|
530
605
|
}
|
|
531
606
|
nlbHandler.addEnvironment('SERVER_NAME', serverName);
|
|
532
607
|
nlbHandler.addEnvironment('NETWORK_NAME', networkName);
|
|
@@ -571,6 +646,39 @@ export class IrcAwsStack extends Stack {
|
|
|
571
646
|
description: 'HTTPS management endpoint for ApiGatewayManagementApi.postToConnection.',
|
|
572
647
|
value: stage.callbackUrl,
|
|
573
648
|
});
|
|
649
|
+
|
|
650
|
+
// -------------------------------------------------------------------------
|
|
651
|
+
// Web client hosting substrate (S3 + CloudFront + OAC).
|
|
652
|
+
//
|
|
653
|
+
// Serves the shared `apps/web/dist` artifact (Kiwi SPA + landing page)
|
|
654
|
+
// exclusively over CloudFront. The S3 origin bucket is private; CloudFront
|
|
655
|
+
// reads it via Origin Access Control (the modern replacement for OAI).
|
|
656
|
+
// Only provisioned when `webSite` is supplied — staging defaults to the
|
|
657
|
+
// `*.cloudfront.net` URL, prod passes the custom-domain fields. Pure
|
|
658
|
+
// infrastructure; the per-env config bake and the s3-sync deploy pipeline
|
|
659
|
+
// are separate.
|
|
660
|
+
// -------------------------------------------------------------------------
|
|
661
|
+
if (props.webSite !== undefined) {
|
|
662
|
+
const site = new StaticSite(this, 'StaticSite', props.webSite);
|
|
663
|
+
// Expose the deploy-pipeline-relevant resource IDs at the PARENT-stack
|
|
664
|
+
// level (stable logical IDs — no CDK hashing, which only kicks in for
|
|
665
|
+
// outputs nested under child constructs). `scripts/deploy-web-aws.mjs`
|
|
666
|
+
// reads these via `aws cloudformation describe-stacks --query` to drive
|
|
667
|
+
// `s3 sync apps/web/dist s3://<bucket> --delete` and
|
|
668
|
+
// `cloudfront create-invalidation --distribution-id <id> --paths /*`.
|
|
669
|
+
// `ConnectUrl` (above) supplies the wss URL that the same script bakes
|
|
670
|
+
// into the SPA's config.json via `build.mjs --api-url`.
|
|
671
|
+
new CfnOutput(this, 'WebsiteBucketName', {
|
|
672
|
+
description:
|
|
673
|
+
'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`.',
|
|
674
|
+
value: site.bucket.bucketName,
|
|
675
|
+
});
|
|
676
|
+
new CfnOutput(this, 'WebsiteDistributionId', {
|
|
677
|
+
description:
|
|
678
|
+
'CloudFront distribution ID serving the web client. The deploy pipeline runs `aws cloudfront create-invalidation --distribution-id <this-id> --paths /*` after each s3 sync.',
|
|
679
|
+
value: site.distribution.distributionId,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
574
682
|
}
|
|
575
683
|
}
|
|
576
684
|
|
|
@@ -582,6 +690,34 @@ function handlerEntry(): string {
|
|
|
582
690
|
return resolve(HERE, '..', '..', '..', 'packages', 'aws-adapter', 'src', 'handlers', 'index.ts');
|
|
583
691
|
}
|
|
584
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Resolves the API Gateway `DataTraceEnabled` flag for the stage from the
|
|
695
|
+
* construct props. Safe-by-default: returns `false` unless the explicit,
|
|
696
|
+
* documented escape hatch is invoked. See {@link IrcStackProps.allowDataTrace}.
|
|
697
|
+
*
|
|
698
|
+
* Post-T170 (staging/prod collapse) every synth is effectively a prod synth,
|
|
699
|
+
* so there is no "non-prod" branch to quietly enable tracing on. The guard
|
|
700
|
+
* therefore refuses synthesis the moment `allowDataTrace` is requested
|
|
701
|
+
* without the explicit `iUnderstandThisLeaksCredentials` acknowledgement —
|
|
702
|
+
* forcing the operator to read the credential-leak warning.
|
|
703
|
+
*/
|
|
704
|
+
function resolveDataTraceEnabled(props: IrcStackProps): boolean {
|
|
705
|
+
if (props.allowDataTrace === true) {
|
|
706
|
+
if (props.iUnderstandThisLeaksCredentials === true) {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
throw new Error(
|
|
710
|
+
'Refusing to enable API Gateway DataTraceEnabled: for a WebSocket IRC ' +
|
|
711
|
+
'endpoint this logs the FULL body of every frame (PASS <password>, ' +
|
|
712
|
+
'AUTHENTICATE <base64-SASL-PLAIN>, JOIN #chan <channel-key>, every ' +
|
|
713
|
+
'PRIVMSG/NOTICE) to the APIGW CloudWatch log group, leaking IRC ' +
|
|
714
|
+
'credentials to anyone with logs:GetLogEvents. To override in a ' +
|
|
715
|
+
'throwaway sandbox ONLY, also pass iUnderstandThisLeaksCredentials=true.',
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
|
|
585
721
|
/**
|
|
586
722
|
* Internal Lambda-construct props: `NodejsFunctionProps` narrowed so the
|
|
587
723
|
* runtime and handler are required. Every call site in the stack already
|
|
@@ -623,14 +759,3 @@ function makeLambda(scope: Construct, id: string, props: StrictLambdaProps): Lam
|
|
|
623
759
|
}
|
|
624
760
|
return new NodejsFunction(scope, id, props);
|
|
625
761
|
}
|
|
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
|
-
}
|