serverless-ircd 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/deploy-aws.yml +1 -3
  3. package/.github/workflows/deploy-cf-tcp.yml +87 -0
  4. package/.github/workflows/deploy-cf.yml +1 -1
  5. package/.node-version +1 -0
  6. package/.nvmrc +1 -0
  7. package/CHANGELOG.md +258 -18
  8. package/README.md +72 -30
  9. package/apps/aws-stack/README.md +2 -2
  10. package/apps/aws-stack/bin/aws.ts +7 -0
  11. package/apps/aws-stack/package.json +4 -4
  12. package/apps/aws-stack/src/aws-stack.ts +118 -6
  13. package/apps/aws-stack/tests/stack.test.ts +98 -3
  14. package/apps/cf-tcp-container/Dockerfile +69 -0
  15. package/apps/cf-tcp-container/package.json +34 -0
  16. package/apps/cf-tcp-container/src/config-loader.ts +145 -0
  17. package/apps/cf-tcp-container/src/container-do.ts +38 -0
  18. package/apps/cf-tcp-container/src/container-server.ts +363 -0
  19. package/apps/cf-tcp-container/src/main.ts +77 -0
  20. package/apps/cf-tcp-container/src/persistence.ts +144 -0
  21. package/apps/cf-tcp-container/src/worker.ts +41 -0
  22. package/apps/cf-tcp-container/terraform/provider.tf +24 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
  25. package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
  26. package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
  27. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
  28. package/apps/cf-tcp-container/tsconfig.build.json +17 -0
  29. package/apps/cf-tcp-container/tsconfig.test.json +15 -0
  30. package/apps/cf-tcp-container/vitest.config.ts +26 -0
  31. package/apps/cf-tcp-container/wrangler.toml +63 -0
  32. package/apps/cf-worker/package.json +1 -1
  33. package/apps/cf-worker/scripts/smoke.mjs +0 -0
  34. package/apps/cf-worker/src/worker.ts +8 -2
  35. package/apps/cf-worker/tests/smoke.test.ts +1 -0
  36. package/apps/cf-worker/wrangler.test.toml +11 -1
  37. package/apps/cf-worker/wrangler.toml +69 -19
  38. package/apps/local-cli/package.json +1 -1
  39. package/apps/local-cli/src/config-loader.ts +11 -0
  40. package/apps/local-cli/src/main.ts +1 -1
  41. package/apps/local-cli/src/server.ts +46 -3
  42. package/apps/local-cli/tests/e2e.test.ts +52 -0
  43. package/package.json +19 -16
  44. package/packages/aws-adapter/package.json +3 -3
  45. package/packages/aws-adapter/src/account-store.ts +121 -0
  46. package/packages/aws-adapter/src/admission.ts +74 -0
  47. package/packages/aws-adapter/src/aws-runtime.ts +124 -34
  48. package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
  49. package/packages/aws-adapter/src/config-loader.ts +62 -0
  50. package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
  51. package/packages/aws-adapter/src/handlers/connect.ts +64 -8
  52. package/packages/aws-adapter/src/handlers/default.ts +43 -2
  53. package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
  54. package/packages/aws-adapter/src/handlers/index.ts +120 -9
  55. package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
  56. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  57. package/packages/aws-adapter/src/index.ts +13 -0
  58. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
  59. package/packages/aws-adapter/tests/account-store.test.ts +279 -0
  60. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  61. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  62. package/packages/aws-adapter/tests/config-loader.test.ts +75 -0
  63. package/packages/aws-adapter/tests/connect.test.ts +78 -0
  64. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
  65. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  66. package/packages/aws-adapter/tests/handlers.test.ts +194 -47
  67. package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
  68. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  69. package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
  70. package/packages/aws-adapter/tests/transactions.test.ts +25 -20
  71. package/packages/cf-adapter/package.json +1 -1
  72. package/packages/cf-adapter/src/cf-runtime.ts +40 -22
  73. package/packages/cf-adapter/src/channel-do.ts +40 -1
  74. package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
  75. package/packages/cf-adapter/src/config-loader.ts +62 -0
  76. package/packages/cf-adapter/src/connection-do.ts +181 -31
  77. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  78. package/packages/cf-adapter/src/env.ts +58 -8
  79. package/packages/cf-adapter/src/index.ts +11 -9
  80. package/packages/cf-adapter/tests/cf-harness.ts +11 -1
  81. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
  82. package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
  83. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  84. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  85. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  86. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  87. package/packages/cf-adapter/tests/connection-do.test.ts +40 -0
  88. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  89. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  90. package/packages/cf-adapter/tests/worker/main.ts +9 -1
  91. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
  92. package/packages/cf-adapter/wrangler.test.toml +18 -1
  93. package/packages/in-memory-runtime/package.json +1 -1
  94. package/packages/in-memory-runtime/src/in-memory-runtime.ts +14 -28
  95. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
  96. package/packages/irc-core/package.json +8 -2
  97. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  98. package/packages/irc-core/src/caps/capabilities.ts +23 -2
  99. package/packages/irc-core/src/case-fold.ts +64 -0
  100. package/packages/irc-core/src/cloak.ts +1 -1
  101. package/packages/irc-core/src/commands/cap.ts +8 -1
  102. package/packages/irc-core/src/commands/index.ts +11 -0
  103. package/packages/irc-core/src/commands/invite.ts +6 -9
  104. package/packages/irc-core/src/commands/ison.ts +61 -0
  105. package/packages/irc-core/src/commands/isupport.ts +1 -1
  106. package/packages/irc-core/src/commands/join.ts +4 -3
  107. package/packages/irc-core/src/commands/kick.ts +4 -11
  108. package/packages/irc-core/src/commands/list.ts +5 -4
  109. package/packages/irc-core/src/commands/mode.ts +5 -9
  110. package/packages/irc-core/src/commands/motd-lines.ts +127 -0
  111. package/packages/irc-core/src/commands/motd.ts +6 -110
  112. package/packages/irc-core/src/commands/oper.ts +151 -0
  113. package/packages/irc-core/src/commands/quit.ts +12 -0
  114. package/packages/irc-core/src/commands/registration.ts +26 -19
  115. package/packages/irc-core/src/commands/sasl.ts +72 -9
  116. package/packages/irc-core/src/commands/server-info.ts +129 -0
  117. package/packages/irc-core/src/commands/tagmsg.ts +205 -0
  118. package/packages/irc-core/src/commands/userhost.ts +84 -0
  119. package/packages/irc-core/src/commands/whowas.ts +113 -0
  120. package/packages/irc-core/src/config.ts +84 -3
  121. package/packages/irc-core/src/credential-hashing.ts +124 -0
  122. package/packages/irc-core/src/effects.ts +6 -29
  123. package/packages/irc-core/src/index.ts +3 -0
  124. package/packages/irc-core/src/ports.ts +240 -7
  125. package/packages/irc-core/src/protocol/numerics.ts +14 -8
  126. package/packages/irc-core/src/types.ts +60 -1
  127. package/packages/irc-core/tests/account-store.test.ts +131 -0
  128. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  129. package/packages/irc-core/tests/case-fold.test.ts +80 -0
  130. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  131. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  132. package/packages/irc-core/tests/commands/kick.test.ts +15 -0
  133. package/packages/irc-core/tests/commands/oper.test.ts +257 -0
  134. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  135. package/packages/irc-core/tests/commands/registration.test.ts +256 -19
  136. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  137. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  138. package/packages/irc-core/tests/commands/tagmsg.test.ts +662 -0
  139. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  140. package/packages/irc-core/tests/commands/who.test.ts +22 -4
  141. package/packages/irc-core/tests/commands/whois.test.ts +8 -1
  142. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  143. package/packages/irc-core/tests/config.test.ts +170 -1
  144. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  145. package/packages/irc-core/tests/effects.test.ts +0 -27
  146. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  147. package/packages/irc-core/tests/numerics.test.ts +12 -0
  148. package/packages/irc-core/tests/types.test.ts +35 -1
  149. package/packages/irc-core/tsconfig.build.json +1 -1
  150. package/packages/irc-core/tsconfig.test.json +1 -1
  151. package/packages/irc-server/package.json +1 -1
  152. package/packages/irc-server/src/actor.ts +182 -14
  153. package/packages/irc-server/src/dispatch.ts +0 -3
  154. package/packages/irc-server/src/index.ts +10 -2
  155. package/packages/irc-server/src/routing.ts +21 -0
  156. package/packages/irc-server/src/transport.ts +101 -0
  157. package/packages/irc-server/tests/actor.test.ts +617 -1
  158. package/packages/irc-server/tests/dispatch.test.ts +0 -17
  159. package/packages/irc-server/tests/routing.test.ts +7 -0
  160. package/packages/irc-server/tests/transport.test.ts +230 -0
  161. package/packages/irc-test-support/package.json +1 -1
  162. package/packages/irc-test-support/src/harness.ts +44 -9
  163. package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
  164. package/packages/irc-test-support/src/index.ts +3 -0
  165. package/packages/irc-test-support/src/scenarios.ts +132 -2
  166. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  167. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  168. package/pnpm-workspace.yaml +9 -0
  169. package/tools/ci-hardening/package.json +1 -1
  170. package/tools/package.json +4 -0
  171. package/tools/seed-aws-accounts.ts +79 -0
  172. package/tools/seed-cf-accounts.ts +104 -0
  173. package/tools/tcp-ws-forwarder/package.json +1 -1
  174. package/AGENTS.md +0 -5
  175. package/MOTD.txt +0 -3
  176. package/PLAN-FIXES.md +0 -358
  177. package/PLAN.md +0 -420
  178. package/dashboards/cloudwatch-irc.json +0 -139
  179. package/docs/AWS-Adapter-Architecture.md +0 -494
  180. package/docs/AWS-Deployment.md +0 -1107
  181. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  182. package/docs/Home.md +0 -1
  183. package/docs/Observability.md +0 -87
  184. package/docs/PlanIRCv3Websocket.md +0 -489
  185. package/docs/PlanWebClient.md +0 -451
  186. package/docs/Release-Process.md +0 -440
  187. package/progress.md +0 -107
  188. package/tickets.md +0 -2485
  189. package/webircgateway/LICENSE +0 -201
  190. package/webircgateway/Makefile +0 -44
  191. package/webircgateway/README.md +0 -134
  192. package/webircgateway/config.conf.example +0 -135
  193. package/webircgateway/go.mod +0 -16
  194. package/webircgateway/go.sum +0 -89
  195. package/webircgateway/main.go +0 -118
  196. package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
  197. package/webircgateway/pkg/identd/identd.go +0 -86
  198. package/webircgateway/pkg/identd/rpcclient.go +0 -59
  199. package/webircgateway/pkg/irc/isupport.go +0 -56
  200. package/webircgateway/pkg/irc/message.go +0 -217
  201. package/webircgateway/pkg/irc/state.go +0 -79
  202. package/webircgateway/pkg/proxy/proxy.go +0 -129
  203. package/webircgateway/pkg/proxy/server.go +0 -237
  204. package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
  205. package/webircgateway/pkg/webircgateway/client.go +0 -741
  206. package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
  207. package/webircgateway/pkg/webircgateway/config.go +0 -385
  208. package/webircgateway/pkg/webircgateway/gateway.go +0 -278
  209. package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
  210. package/webircgateway/pkg/webircgateway/hooks.go +0 -152
  211. package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
  212. package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
  213. package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
  214. package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
  215. package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
  216. package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
  217. package/webircgateway/pkg/webircgateway/utils.go +0 -147
  218. package/webircgateway/plugins/example/plugin.go +0 -11
  219. package/webircgateway/plugins/stats/plugin.go +0 -52
  220. package/webircgateway/staticcheck.conf +0 -1
  221. package/webircgateway/webircgateway.svg +0 -3
@@ -1,1107 +0,0 @@
1
- # AWS Deployment Guide
2
-
3
- End-to-end guide for deploying ServerlessIRCd to AWS (Phase 4 of the
4
- project plan). Covers prerequisites, first-time AWS setup, local
5
- development, staging and production deploys, configuration reference,
6
- DynamoDB capacity planning, region strategy, cost notes, CI/CD, and
7
- troubleshooting.
8
-
9
- Cross-reference: the AWS Adapter Architecture doc
10
- (`../../ServerlessIRCd/docs/AWS-Adapter-Architecture.md`) for the
11
- diagrams referenced throughout, and the Cloudflare Deployment Guide
12
- (`../../ServerlessIRCd/docs/Cloudflare-Deployment-Guide.md`) for the
13
- equivalent flow on the other supported platform. The shared
14
- `irc-core`/`irc-server` packages and the contract test suite are
15
- identical between platforms; only the I/O shell (this stack) differs.
16
-
17
- Key in-repo paths cited below:
18
-
19
- | Path | What |
20
- |---------------------------------------------------|-----------------------------------------------|
21
- | `apps/aws-stack/src/aws-stack.ts` | CDK stack — tables, Lambdas, APIGW, schedules.|
22
- | `apps/aws-stack/bin/aws.ts` | CDK app entry. Stack id is `IrcAwsStack`. |
23
- | `apps/aws-stack/cdk.json` | `app = tsx bin/aws.ts`. |
24
- | `apps/aws-stack/scripts/smoke.mjs` | CI + local smoke e2e. |
25
- | `apps/aws-stack/tests/stack.test.ts` | Synth-time assertions (cross-check for facts).|
26
- | `packages/aws-adapter/src/cdk-table-defs.ts` | Five DynamoDB table shapes. |
27
- | `packages/aws-adapter/src/config-loader.ts` | Lambda env → `ServerConfig` mapping. |
28
- | `packages/aws-adapter/src/handlers/index.ts` | Lambda entry (`handler`, `sweeperHandler`, `pingCheckerHandler`). |
29
- | `.github/workflows/deploy-aws.yml` | Staging deploy + smoke e2e CI. |
30
-
31
- **Acceptance criterion:** a new contributor can deploy their own
32
- staging instance following only this doc.
33
-
34
- ---
35
-
36
- ## 1. What gets deployed
37
-
38
- A single CDK v2 stack (`IrcAwsStack`) provisions the entire AWS
39
- substrate. There are no static assets — CloudFront and S3 are not part
40
- of this deployment. Every resource is regional.
41
-
42
- ```
43
- ┌──────────────────────────────────────────────────────────────────┐
44
- │ CDK stack: IrcAwsStack (apps/aws-stack/src/aws-stack.ts) │
45
- │ │
46
- │ ┌─ WebSocketApi (AWS::ApiGatewayV2::Api) ───────────────────┐ │
47
- │ │ routes: $connect $disconnect $default │ │
48
- │ │ → all three wired to IrcHandler │ │
49
- │ │ stage: prod (auto-deploy) │ │
50
- │ └────────────────────────────────────────────────────────────┘ │
51
- │ │ │
52
- │ ▼ WebSocket event per frame │
53
- │ ┌─ Lambda: IrcHandler ───────────────────────────────────────┐ │
54
- │ │ handler export `handler` (Node 20, esbuild bundle) │ │
55
- │ │ entry: packages/aws-adapter/src/handlers/index.ts │ │
56
- │ │ dispatches on event.requestContext.routeKey │ │
57
- │ └────────────────────────────────────────────────────────────┘ │
58
- │ │
59
- │ ┌─ Lambda: IrcSweeper ───────────────────────────────────────┐ │
60
- │ │ handler export `sweeperHandler` │ │
61
- │ │ EventBridge schedule: rate(5 minutes) │ │
62
- │ │ Scans Connections for rows past hard TTL; cleans up. │ │
63
- │ └────────────────────────────────────────────────────────────┘ │
64
- │ │
65
- │ ┌─ Lambda: IrcPingChecker ───────────────────────────────────┐ │
66
- │ │ handler export `pingCheckerHandler` │ │
67
- │ │ EventBridge schedule: rate(1 minute) │ │
68
- │ │ Sends PING to idle connections; disconnects no-PONG. │ │
69
- │ └────────────────────────────────────────────────────────────┘ │
70
- │ │
71
- │ ┌─ DynamoDB (on-demand) ─────────────────────────────────────┐ │
72
- │ │ Connections (PK connectionId, TTL idleSince) │ │
73
- │ │ ChannelMeta (PK channelName) │ │
74
- │ │ ChannelMembers (PK channelName, SK connectionId) │ │
75
- │ │ Nicks (PK nickLower) │ │
76
- │ │ Accounts (PK account) │ │
77
- │ └────────────────────────────────────────────────────────────┘ │
78
- │ │
79
- │ CfnOutput: ConnectUrl = stage.url (wss://…) │
80
- │ CfnOutput: ManagementUrl = stage.callbackUrl (https://…) │
81
- └──────────────────────────────────────────────────────────────────┘
82
- ```
83
-
84
- All IRC protocol logic lives in the shared `irc-core` reducers and the
85
- `irc-server` `ConnectionActor`. The Lambdas are a thin I/O shell: they
86
- forward WebSocket events into the actor and interpret the resulting
87
- `Effect[]` against DynamoDB and the API Gateway Management API. No IRC
88
- state is held in the Lambda process between invocations.
89
-
90
- The three Lambda functions share a single bundle (esbuild tree-shakes
91
- per handler export at synth time). They run on `nodejs20.x` and exclude
92
- `@aws-sdk/*` from the bundle because the AWS SDK v3 ships with the
93
- Lambda Node 20 runtime.
94
-
95
- Stack outputs (`CfnOutput`):
96
-
97
- | Output | Value | Purpose |
98
- |-----------------|-----------------------------|----------------------------------------------------|
99
- | `ConnectUrl` | `wss://<id>.execute-api.<region>.amazonaws.com/prod` | WebSocket endpoint IRC clients dial. |
100
- | `ManagementUrl` | `https://<id>.execute-api.<region>.amazonaws.com/prod` | HTTPS endpoint for `ApiGatewayManagementApi.postToConnection`. |
101
-
102
- The same CloudFormation template serves both staging and production.
103
- The staging/production distinction today is which AWS account and
104
- region the stack is deployed into — there is no per-environment stack
105
- parameter (see §6 and §7.4 for the implications).
106
-
107
- ---
108
-
109
- ## 2. Prerequisites
110
-
111
- | Requirement | Version / detail |
112
- |---------------------|---------------------------------------------------------------|
113
- | Node.js | ≥ 20 (matches CI; `engines.node` in root `package.json` and in `apps/aws-stack/package.json`). |
114
- | pnpm | 9.x (`packageManager: pnpm@9.15.9` in root `package.json`). |
115
- | AWS account | IAM-permissioned to create CloudFormation stacks, IAM roles, API Gateway v2, Lambda, DynamoDB tables, and EventBridge rules. |
116
- | AWS credentials | `aws configure`, `AWS_PROFILE`, or SSO. CDK reads the standard SDK chain. |
117
- | CDK v2 | Comes from `apps/aws-stack/devDependencies` (`aws-cdk ^2.160.0`) — no global install needed. Use `pnpm cdk` (or `pnpm cdk:synth`) so the local binary wins over any globally-installed CDK. |
118
- | CDK bootstrap | `cdk bootstrap` must have been run **once** in the target account/region (see §3.3). |
119
- | Git checkout | Clean working tree on `main` for production deploys. |
120
-
121
- Confirm the local environment:
122
-
123
- ```bash
124
- node --version # v20.x or newer
125
- pnpm --version # 9.x
126
- aws --version # aws-cli/2.x (any 2.x is fine)
127
- pnpm install # from repo root, installs the whole workspace
128
- pnpm build # builds irc-core, irc-server, aws-adapter, aws-stack
129
- ```
130
-
131
- `pnpm build` must succeed before `cdk deploy`: the `NodejsFunction`
132
- construct transpiles the handler entry with esbuild at synth time, and
133
- esbuild resolves `@serverless-ircd/*` workspace packages via their
134
- compiled `dist/` outputs.
135
-
136
- ---
137
-
138
- ## 3. First-time AWS setup
139
-
140
- These steps are done once per AWS account.
141
-
142
- ### 3.1 Pick a region
143
-
144
- API Gateway WebSocket endpoints, DynamoDB, EventBridge Scheduler, and
145
- Lambda are all regional. WebSocket clients pay round-trip time to the
146
- region, so pick the one closest to your user base (see §10 for the
147
- full region discussion). Set it as your default:
148
-
149
- ```bash
150
- export AWS_REGION=us-east-1 # or your nearest region
151
- ```
152
-
153
- CDK picks the region up via `CDK_DEFAULT_REGION` (set automatically by
154
- the CDK CLI from the SDK chain).
155
-
156
- ### 3.2 Configure credentials
157
-
158
- Three equivalent options:
159
-
160
- ```bash
161
- # Static access key (simplest):
162
- aws configure
163
- # → prompts for Access Key ID, Secret Key, region, output format.
164
-
165
- # Named profile:
166
- export AWS_PROFILE=my-profile
167
-
168
- # SSO:
169
- aws sso login --profile my-sso-profile
170
- export AWS_PROFILE=my-sso-profile
171
- ```
172
-
173
- Verify:
174
-
175
- ```bash
176
- aws sts get-caller-identity
177
- ```
178
-
179
- ### 3.3 Bootstrap CDK
180
-
181
- CDK needs an S3 bucket + a few IAM roles per account/region to hold
182
- synthesized CloudFormation assets. Bootstrap once:
183
-
184
- ```bash
185
- cd apps/aws-stack
186
- pnpm cdk bootstrap
187
- # → creates a CFN stack named CDKToolkit with the bootstrap bucket.
188
- ```
189
-
190
- If you skip this, the first `cdk deploy` fails with a bucket-not-found
191
- error (see §13.1). Bootstrap is idempotent — re-running on an
192
- already-bootstrapped account is a no-op.
193
-
194
- ### 3.4 Optional: set up OIDC for CI
195
-
196
- If you intend to use the GitHub Actions deploy workflow (§12) without
197
- long-lived access keys, configure GitHub OIDC:
198
-
199
- 1. Create an IAM identity provider for `token.actions.githubusercontent.com`.
200
- 2. Create a role with a trust policy granting
201
- `sts:AssumeRoleWithWebIdentity` to your repo's `ref:refs/heads/main`.
202
- 3. Attach a permission policy broad enough to manage the stack (IAM,
203
- API Gateway v2, Lambda, DynamoDB, EventBridge, CloudFormation).
204
- 4. Note the role ARN — it becomes the `AWS_DEPLOY_ROLE_ARN` secret in
205
- CI. Static keys (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`) are
206
- the fallback; the workflow supports both.
207
-
208
- ---
209
-
210
- ## 4. Local development
211
-
212
- Unlike the Cloudflare adapter (which has `wrangler dev` against the
213
- real `workerd` runtime), the AWS adapter does not yet ship a
214
- hot-reload local Lambda/API Gateway emulator. The local dev loop is:
215
-
216
- 1. **Synth-time assertions** (default `pnpm test`) — fast, no Docker.
217
- 2. **Unit tests** for the handler, runtime, and access patterns under
218
- `packages/aws-adapter/tests/`.
219
- 3. **Optional localstack validation** — gated, requires Docker.
220
-
221
- ### 4.1 Synth-time tests
222
-
223
- From the repo root:
224
-
225
- ```bash
226
- pnpm --filter @serverless-ircd/aws-stack test
227
- # or, equivalently:
228
- pnpm test # runs the whole workspace including aws-stack
229
- ```
230
-
231
- These execute `Template.fromStack(...)` in-process — no AWS
232
- credentials, no Docker. Every assertion in
233
- `apps/aws-stack/tests/stack.test.ts` corresponds to a fact this guide
234
- states: five on-demand tables, three Node 20 Lambdas, the
235
- `$connect`/`$disconnect`/`$default` routes, the `prod` stage, the two
236
- `rate(...)` schedules, the two CfnOutputs, and least-privilege IAM
237
- (no `Action: "*"` or `Resource: "*"`).
238
-
239
- ### 4.2 Synthesize the CloudFormation template
240
-
241
- ```bash
242
- pnpm --filter @serverless-ircd/aws-stack cdk:synth
243
- # → writes cdk.out/<stack>.template.json
244
- ```
245
-
246
- Inspecting `cdk.out/` is the fastest way to see exactly what will be
247
- deployed without touching AWS.
248
-
249
- ### 4.3 Optional: validate against localstack
250
-
251
- A gated test (`apps/aws-stack/tests/localstack.test.ts`) runs
252
- `cdklocal synth` against a real localstack container and re-asserts
253
- the five-table shape. It is **skipped unless `LOCALSTACK=1` is set** so
254
- CI never depends on Docker. To run it locally:
255
-
256
- ```bash
257
- # 1. Start localstack (requires Docker):
258
- docker run --rm -d -p 4566:4566 localstack/localstack
259
-
260
- # 2. Install the localstack-aware CDK CLI globally:
261
- npm install -g aws-cdk-local
262
-
263
- # 3. Point the AWS SDK at localstack (cdklocal reads these):
264
- export AWS_ACCESS_KEY_ID=test
265
- export AWS_SECRET_ACCESS_KEY=test
266
- export AWS_DEFAULT_REGION=us-east-1
267
-
268
- # 4. Run the gated test:
269
- LOCALSTACK=1 pnpm --filter @serverless-ircd/aws-stack test
270
- ```
271
-
272
- ### 4.4 Running the smoke script locally
273
-
274
- The smoke e2e (`apps/aws-stack/scripts/smoke.mjs`) defaults to
275
- `ws://localhost:8787`, inherited from the Cloudflare adapter's dev
276
- port. To smoke-test the AWS adapter locally you would need to stand up
277
- an API Gateway WebSocket emulator in front of the Lambda handler
278
- (sam-cli + localstack can do this); that wiring is not preconfigured.
279
- In practice, local iteration uses the unit + synth tests, and the
280
- smoke script is run against a deployed staging URL (§5.2) or against
281
- the Cloudflare adapter's local dev server when you're iterating on the
282
- shared protocol layer.
283
-
284
- ---
285
-
286
- ## 5. Deploy staging
287
-
288
- Staging is the environment CI deploys on every push to `main`; you can
289
- also deploy it manually from a clean checkout.
290
-
291
- ### 5.1 Deploy from your machine
292
-
293
- ```bash
294
- # From the repo root:
295
- pnpm deploy:aws:staging
296
- # Equivalent to:
297
- # pnpm --filter @serverless-ircd/aws-stack deploy:staging
298
- # → cdk deploy --all --require-approval never
299
- ```
300
-
301
- The first deploy of a given account/region will:
302
-
303
- 1. Upload the esbuild-bundled handler asset to the CDK bootstrap S3
304
- bucket.
305
- 2. Create the `IrcAwsStack` CloudFormation stack.
306
- 3. Provision five DynamoDB tables, three Lambda functions, one API
307
- Gateway WebSocket API, one `prod` stage, two EventBridge rules,
308
- least-privilege IAM roles, and the two stack outputs.
309
- 4. Print the deployed URL on success — look for the `ConnectUrl`
310
- output, e.g.
311
- `wss://abc123.execute-api.us-east-1.amazonaws.com/prod`.
312
-
313
- A typical first deploy takes 2–4 minutes (DynamoDB table creation and
314
- API Gateway stage auto-deploy dominate).
315
-
316
- ### 5.2 Verify with the smoke script
317
-
318
- ```bash
319
- node apps/aws-stack/scripts/smoke.mjs \
320
- --url wss://abc123.execute-api.us-east-1.amazonaws.com/prod
321
- ```
322
-
323
- The script replays CONNECT → NICK/USER → JOIN #smoke → PRIVMSG → QUIT
324
- and asserts the server emits `001`, `376`, `353`, `366`, and closes
325
- the socket. Expected output on success:
326
-
327
- ```json
328
- {"ts":"...","level":"info","msg":"aws-stack smoke e2e passed","url":"wss://..."}
329
- ```
330
-
331
- On failure the script writes a `level: "error"` JSON line and exits
332
- non-zero, which is what the CI workflow gates on.
333
-
334
- ### 5.3 Connect a real IRC client
335
-
336
- Point any RFC-compliant WebSocket-aware IRC client at the deployed
337
- `wss://` URL. For WeeChat:
338
-
339
- ```
340
- /server add ircd abc123.execute-api.us-east-1.amazonaws.com/443
341
- /set irc.server.ircd.ssl on
342
- /connect ircd
343
- /join #test
344
- ```
345
-
346
- For a quick text-only check, the `apps/local-cli` and
347
- `tools/tcp-ws-forwarder` packages in the parent repo bridge a plain
348
- TCP IRC client to a WebSocket endpoint.
349
-
350
- ---
351
-
352
- ## 6. Deploy production
353
-
354
- The repo-level shortcut is:
355
-
356
- ```bash
357
- pnpm deploy:aws:prod
358
- # Equivalent to:
359
- # pnpm --filter @serverless-ircd/aws-stack deploy:prod
360
- # → cdk deploy --all --require-approval never
361
- ```
362
-
363
- **There is no separate production stack.** `deploy:prod` and
364
- `deploy:staging` run the identical CDK command — both deploy a stack
365
- named `IrcAwsStack` (hardcoded in `apps/aws-stack/bin/aws.ts`). The
366
- staging/production distinction is **which AWS account and region the
367
- stack is deployed into**, controlled by the credentials and
368
- `AWS_REGION` in the environment.
369
-
370
- For a real production deployment, before your first `pnpm deploy:aws:prod`:
371
-
372
- 1. **Use a dedicated AWS account** (or at least a dedicated region) for
373
- production. Do not point production credentials at the same stack
374
- CI is deploying to on every push.
375
- 2. **Change `RemovalPolicy.DESTROY` to `RemovalPolicy.RETAIN`** on the
376
- five DynamoDB tables (see §9.4). The committed stack deletes all
377
- data on `cdk destroy` — intentional for staging, dangerous for
378
- production.
379
- 3. **Override the server identity** (`SERVER_NAME`, `NETWORK_NAME`,
380
- `MOTD`) — see §7.1. The committed defaults are placeholders.
381
- 4. **Pick a region close to your users** (see §10).
382
-
383
- There is **no CI auto-deploy to production**. The deploy workflow
384
- (`deploy-aws.yml`) only triggers on push to `main` and targets
385
- staging. Production deploys are always manual from a clean checkout.
386
-
387
- ---
388
-
389
- ## 7. Configuration reference
390
-
391
- Server identity (`SERVER_NAME`, `NETWORK_NAME`, `MOTD`) and the logical
392
- environment name are CDK construct props on `IrcAwsStack`
393
- (`apps/aws-stack/src/aws-stack.ts`); pass them when instantiating the
394
- stack. All other deployment knobs still live in the stack source.
395
- Unlike the Cloudflare adapter (where config lives in a declarative
396
- `wrangler.toml`), AWS config is code — non-identity overrides require
397
- editing the stack and re-deploying.
398
-
399
- ### 7.1 Server identity (construct props)
400
-
401
- The three identity values are sourced from `IrcStackProps` and joined
402
- to the env vars the Lambda config loader reads. `motdLines` is a
403
- `string[]`; the stack joins it with `\n` so the loader splits it back
404
- into one entry per IRC numeric (`375` / `372` / `376`):
405
-
406
- ```ts
407
- // apps/aws-stack/src/aws-stack.ts (excerpt)
408
- const motd = (props.motdLines ?? DEFAULT_MOTD_LINES).join('\n');
409
- handler.addEnvironment('SERVER_NAME', serverName);
410
- handler.addEnvironment('NETWORK_NAME', networkName);
411
- handler.addEnvironment('MOTD', motd);
412
- ```
413
-
414
- These three values are injected into the `IrcHandler` and
415
- `IrcPingChecker` Lambda environments at synth time. To override them,
416
- pass the construct props when creating the stack:
417
-
418
- ```ts
419
- new IrcAwsStack(app, 'IrcAwsStack-production', {
420
- environmentName: 'production',
421
- serverName: 'irc.my.net',
422
- networkName: 'MyNet',
423
- motdLines: ['Welcome to my network.', 'Be excellent to each other.'],
424
- });
425
- ```
426
-
427
- | Env var | Purpose | Committed default |
428
- |-----------------|-----------------------------------------------|------------------------------------------------|
429
- | `SERVER_NAME` | Server name sent in `001`/`005` numerics. | `irc.example.com` (placeholder — override!) |
430
- | `NETWORK_NAME` | Network label in `005 NETWORK=…`. | `ExampleNet` |
431
- | `MOTD` | Message-of-the-day. **Delimiter: `\n`** — the loader splits the value on every newline into one MOTD line. | Two-line banner (see note below). |
432
-
433
- > **MOTD delimiter contract:** the config loader
434
- > (`packages/aws-adapter/src/config-loader.ts`) splits `MOTD` on `\n`
435
- > to form the multi-line MOTD array. The CDK stack's `motdLines`
436
- > construct prop is a `string[]` joined with `\n`, so each array entry
437
- > becomes its own line and round-trips back through the loader into the
438
- > expected number of `motdLines`. Do **not** use a comma (or any other
439
- > character) as a separator — the loader treats it as literal line
440
- > content, collapsing the MOTD into a single line.
441
-
442
- ### 7.2 Lambda runtime config (injected by CDK, do not edit)
443
-
444
- These env vars are wired automatically by the stack at synth time. Do
445
- not hand-set them in the console — the next `cdk deploy` will
446
- overwrite them.
447
-
448
- | Env var | Source | Consumed by |
449
- |------------------------|-------------------------------------------|--------------------------------------|
450
- | `MANAGEMENT_URL` | `stage.callbackUrl` (CfnOutput `ManagementUrl`) | `IrcHandler`, `IrcPingChecker` (builds `ApiGatewayManagementApi` for `postToConnection`). |
451
- | `CONNECTIONS_TABLE` | `Connections` (physical table name) | All three Lambdas (`tablesConfigFromEnv`). |
452
- | `CHANNELMETA_TABLE` | `ChannelMeta` | All three Lambdas. |
453
- | `CHANNELMEMBERS_TABLE` | `ChannelMembers` | All three Lambdas. |
454
- | `NICKS_TABLE` | `Nicks` | All three Lambdas. |
455
- | `ACCOUNTS_TABLE` | `Accounts` | All three Lambdas. |
456
- | `AWS_REGION` | Injected by the Lambda runtime, not CDK. | `buildDepsFromEnv` (constructs the DynamoDB client). |
457
-
458
- The `tablesConfigFromEnv` helper
459
- (`packages/aws-adapter/src/handlers/index.ts:226`) throws on cold
460
- start if any of the five `<NAME>_TABLE` vars is missing. The smoke
461
- timeout in §13.2 traces back to this.
462
-
463
- ### 7.3 Optional config-loader env vars (read if present)
464
-
465
- The shared config loader (`packages/aws-adapter/src/config-loader.ts`)
466
- also reads these optional env vars and feeds them through the shared
467
- Zod `ServerConfig` schema. **The committed CDK stack does not inject
468
- any of them** — to enable one, add an `addEnvironment(...)` line in
469
- `aws-stack.ts` and re-deploy.
470
-
471
- | Env var | Type | Schema field |
472
- |---------------------------|---------|-----------------------|
473
- | `MAX_CLIENTS` | int | `maxClients` |
474
- | `CHANNEL_PREFIXES` | string | `channelPrefixes` |
475
- | `OPER_USER` | string | `operCreds[0].user` |
476
- | `OPER_PASSWORD` | string | `operCreds[0].password` |
477
- | `MAX_CHANNELS_PER_USER` | int | `maxChannelsPerUser` |
478
- | `MAX_TARGETS_PER_COMMAND` | int | `maxTargetsPerCommand`|
479
- | `NICK_LEN` | int | `nickLen` |
480
- | `CHANNEL_LEN` | int | `channelLen` |
481
- | `TOPIC_LEN` | int | `topicLen` |
482
- | `MAX_LIST_ENTRIES` | int | `maxListEntries` |
483
- | `QUIT_MESSAGE` | string | `quitMessage` |
484
-
485
- Invalid values cause a readable boot-time error — the Lambda will
486
- throw on the first invocation (cold start). Test changes locally with
487
- `pnpm --filter @serverless-ircd/aws-adapter test`.
488
-
489
- ### 7.4 No per-environment stack parameter
490
-
491
- There is no `stageName` or `environmentType` parameter on the CDK
492
- stack. The stack name (`IrcAwsStack`), table names
493
- (`Connections`, `ChannelMeta`, `ChannelMembers`, `Nicks`, `Accounts`),
494
- API Gateway stage name (`prod`), and Lambda construct ids are all
495
- hardcoded. To run staging and production in **the same AWS account**,
496
- you would need to fork the stack to parameterize these names; the
497
- recommended path is to use separate accounts (or separate regions) for
498
- staging vs production. This is the same architectural choice the
499
- Cloudflare adapter makes (staging and production are separate Worker
500
- namespaces).
501
-
502
- ### 7.5 Lambda runtime and bundling
503
-
504
- | Knob | Value |
505
- |-------------------|------------------------------------------------------------|
506
- | Runtime | `nodejs20.x` (`Runtime.NODEJS_20_X`). |
507
- | Entry | `packages/aws-adapter/src/handlers/index.ts` (computed by `handlerEntry()` in `aws-stack.ts`). |
508
- | Handler exports | `IrcHandler` → `handler`; `IrcSweeper` → `sweeperHandler`; `IrcPingChecker` → `pingCheckerHandler`. |
509
- | Bundler | esbuild (via `NodejsFunction`). |
510
- | External modules | `@aws-sdk/*` (ships with the runtime; keeps the bundle small). |
511
- | Memory / timeout | CDK defaults (128 MB / 3 s). Override via `memorySize`/`timeout` props if you observe cold-start or fanout timeouts. |
512
-
513
- ### 7.6 API Gateway stage
514
-
515
- | Knob | Value |
516
- |-------------------|------------------------------------------------------------|
517
- | Stage name | `prod` (hardcoded as `STAGE_NAME` in `aws-stack.ts`). |
518
- | Auto-deploy | `true` (changes propagate without a manual deployment). |
519
- | Auth on `$connect`| None today (no authorizer). See §13.5. |
520
- | Route responses | Default (`$connect` returns 200 → upgrade; `$default` and `$disconnect` are fire-and-forget). |
521
-
522
- ---
523
-
524
- ## 8. Secrets
525
-
526
- **As of v1 the Lambda consumes no secret bindings.** Server password,
527
- SASL `AccountStore` hashes, and oper creds are handled by later
528
- hardening work; when they ship, the natural homes are AWS Secrets
529
- Manager or SSM Parameter Store (the AWS adapter architecture doc
530
- calls both out as supporting services).
531
-
532
- When secrets land, the typical pattern will be:
533
-
534
- 1. Create the secret in Secrets Manager (or a `SecureString`
535
- parameter in SSM).
536
- 2. Grant the Lambda role `secretsmanager:GetSecretValue` (or
537
- `ssm:GetParameter`) scoped to that one secret ARN.
538
- 3. Read it inside `buildDepsFromEnv` (cold start) — never inside the
539
- per-frame hot path.
540
-
541
- ```bash
542
- aws secretsmanager create-secret \
543
- --name serverless-ircd/staging/SERVER_PASSWORD \
544
- --secret-string "$(openssl rand -base64 32)"
545
- ```
546
-
547
- **Never** put credentials, password hashes, or API tokens in
548
- `aws-stack.ts`, in Lambda env vars, or in committed files. Lambda env
549
- vars are visible in the console and in CloudFormation describe output;
550
- they are not secret.
551
-
552
- The CI deploy uses **GitHub Actions secrets** plus an optional repo
553
- variable (see §12):
554
-
555
- | Secret | Used by | Purpose |
556
- |-------------------------|-------------------|---------------------------------------------------------------|
557
- | `AWS_ACCESS_KEY_ID` | `deploy-aws.yml` | Static access key for CDK deploy. Required unless OIDC is used. |
558
- | `AWS_SECRET_ACCESS_KEY` | `deploy-aws.yml` | Companion secret key. Required unless OIDC is used. |
559
- | `AWS_DEPLOY_ROLE_ARN` | `deploy-aws.yml` | Optional. When set, the job assumes this role via OIDC web identity instead of using static keys. |
560
- | `AWS_REGION` | `deploy-aws.yml` | Region to deploy into (e.g. `us-east-1`). |
561
-
562
- | Variable | Purpose |
563
- |-----------------|---------------------------------------------------------------------|
564
- | `AWS_SMOKE_URL` | Optional. `wss://` URL for the smoke e2e step. If unset, the workflow falls back to reading the stack's `ConnectUrl` CloudFormation output via the AWS CLI; if that also fails the smoke step is skipped with a warning. |
565
-
566
- Configure all of these under repo **Settings → Secrets and variables
567
- → Actions**.
568
-
569
- ---
570
-
571
- ## 9. DynamoDB capacity planning
572
-
573
- ### 9.1 All five tables run on-demand
574
-
575
- Every table is created with `BillingMode.PAY_PER_REQUEST` (asserted in
576
- `apps/aws-stack/tests/stack.test.ts:45`). On-demand pricing charges
577
- per request:
578
-
579
- - **Write request units (WRU)** — one WRU = one write of up to 1 KB
580
- (or 2 WRU for a strongly-consistent write of up to 1 KB). A
581
- conditional write that fails (e.g. `attribute_not_exists` rejects a
582
- nick collision) still costs 1 WRU.
583
- - **Read request units (RRU)** — one RRU = one eventually-consistent
584
- read of up to 4 KB (or 2 RRU for strongly consistent).
585
-
586
- There is no minimum spend and no capacity to plan at deploy time.
587
- This is intentional for v1 — it removes a class of
588
- `ProvisionedThroughputExceededException` failure modes. See §11 for
589
- the cost trade-off and when to consider provisioned capacity.
590
-
591
- ### 9.2 Schema and access patterns
592
-
593
- Five tables (defined in
594
- `packages/aws-adapter/src/cdk-table-defs.ts`):
595
-
596
- | Table | PK (HASH) | SK (RANGE) | TTL | Hottest access pattern |
597
- |-----------------|-------------------|------------------|-------------|----------------------------------------------------------|
598
- | `Connections` | `connectionId` | — | `idleSince` | `GetItem`/`UpdateItem` on every frame; `Scan` by sweeper. |
599
- | `ChannelMeta` | `channelName` | — | — | `GetItem`/`UpdateItem` on MODE/TOPIC/KICK. Low volume. |
600
- | `ChannelMembers`| `channelName` | `connectionId` | — | **`Query` on every channel PRIVMSG for fanout.** Hottest. |
601
- | `Nicks` | `nickLower` | — | — | `PutItem` (conditional) on NICK; `GetItem` on PRIVMSG to a nick. |
602
- | `Accounts` | `account` | — | — | `GetItem` on SASL AUTHENTICATE. Cold most of the time. |
603
-
604
- Uniqueness invariants are enforced structurally (not in application
605
- code):
606
-
607
- - `Nicks.PK = nickLower` — conditional `PutItem` with
608
- `attribute_not_exists(PK)` makes nick reservation atomic across
609
- concurrent Lambdas. Two clients racing on `Alice` both get evaluated
610
- against the same row; exactly one wins.
611
- - `ChannelMembers` PK+SK composite — `TransactWriteItems` for JOIN/PART
612
- updates `Connections.joined` and `ChannelMembers` in one atomic
613
- write (up to 100 items per transaction, the AWS limit).
614
- - `Accounts.passHash` — never plaintext; SASL PLAIN verifies against
615
- this hash server-side.
616
-
617
- ### 9.3 The hottest pattern: channel fanout
618
-
619
- Channel PRIVMSG is the dominant cost driver. There is no long-lived
620
- per-channel process (contrast with the Cloudflare adapter's
621
- `ChannelDO`). Fanout is computed per message:
622
-
623
- 1. The reducer emits `Broadcast("#chan", [line], except=sender)`.
624
- 2. `AwsRuntime.broadcast()` issues a `Query` on `ChannelMembers` with
625
- `PK = "#chan"` → returns one row per member.
626
- 3. For each member, the Lambda calls
627
- `ApiGatewayManagementApi.postToConnection(member, line)`.
628
-
629
- That's O(members) Management API calls per message. A 100-member
630
- channel receiving one PRIVMSG per second produces ~100 `postToConnection`
631
- calls/second and one `Query`/second. A 1k-member announcement channel
632
- at the same rate produces ~1k calls/second. Mitigations (batched
633
- postToConnection, per-channel send-list cache, a dedicated fanout
634
- Lambda) are deferred to post-v1; the load-test ticket will surface
635
- the ceiling.
636
-
637
- ### 9.4 Removal policy (IMPORTANT)
638
-
639
- The stack sets `RemovalPolicy.DESTROY` on every table:
640
-
641
- ```ts
642
- // apps/aws-stack/src/aws-stack.ts:48
643
- const tables = Object.entries(TABLE_DEFS).map(
644
- ([logicalId, tableProps]) =>
645
- new Table(this, logicalId, { ...tableProps, removalPolicy: RemovalPolicy.DESTROY }),
646
- );
647
- ```
648
-
649
- **This means `cdk destroy` deletes every table and all of its data.**
650
- This is intentional for staging (you want a clean teardown between
651
- experiments) and **dangerous for production**.
652
-
653
- For production, fork the stack to use `RemovalPolicy.RETAIN` (or
654
- `RETAIN_ON_UPDATE`): tables become orphans on stack deletion and keep
655
- their data. There is no built-in knob for this today — it requires
656
- editing `aws-stack.ts`. Recommendation: ship a `production: boolean`
657
- stack prop in a future change that flips the removal policy. Flagged
658
- in §14.
659
-
660
- ### 9.5 TTL on `Connections.idleSince`
661
-
662
- The `Connections` table carries a DynamoDB TTL attribute (`idleSince`),
663
- asserted at synth time (`stack.test.ts:52`). The Lambda updates
664
- `idleSince` on every frame; DynamoDB silently deletes rows whose
665
- `idleSince` is older than the TTL window (typically within 48 hours of
666
- expiry, no SLA). This is a **safety net** for rows the sweeper misses.
667
-
668
- ### 9.6 The gone-connection sweeper
669
-
670
- A second Lambda (`IrcSweeper`, handler export `sweeperHandler`) runs on
671
- an EventBridge schedule at `rate(5 minutes)`:
672
-
673
- ```ts
674
- new Rule(this, 'SweeperSchedule', {
675
- schedule: Schedule.rate(Duration.minutes(5)),
676
- targets: [new LambdaFunction(sweeper)],
677
- });
678
- ```
679
-
680
- Each tick `Scan`s the entire `Connections` table, identifies rows past
681
- a hard stale threshold, and runs the same cleanup path as
682
- `$disconnect` (delete memberships, release nicks, delete the row). The
683
- sweeper catches connections that vanished without APIGW emitting
684
- `$disconnect` (e.g. TCP RST that APIGW didn't observe).
685
-
686
- **Cost note:** `Scan` reads the **entire** table every 5 minutes. At
687
- 1 KB per row, scanning 10k connections costs ~3 RRU (10k / 4 KB per
688
- RRU) per tick, ~864 ticks/day, ~2.6k RRU/day — negligible. At 100k
689
- connections it's ~26k RRU/day — still small but visible. A future
690
- optimization is to make the sweeper `Query` a sparse GSI keyed by
691
- `idleSince` instead of `Scan`.
692
-
693
- ### 9.7 When to consider provisioned capacity
694
-
695
- Switch a table to `PROVISIONED` billing (with or without autoscaling)
696
- when its traffic is predictable and steady. The clearest signal is
697
- when the on-demand bill for a single table exceeds the cost of
698
- provisioned capacity for the same throughput — DynamoDB's pricing page
699
- breaks this out. In practice:
700
-
701
- - `ChannelMembers` is the first candidate (channel fanout dominates).
702
- - `Connections` is a poor candidate (spiky, cold-start-driven).
703
- - `Accounts`/`ChannelMeta`/`Nicks` are unlikely to need provisioned
704
- capacity in any v1-size deployment.
705
-
706
- Switching billing modes is a no-downtime `UpdateTable` operation; the
707
- stack change is `billingMode: BillingMode.PROVISIONED` plus
708
- `readCapacity`/`writeCapacity` props.
709
-
710
- ---
711
-
712
- ## 10. Region strategy
713
-
714
- ### 10.1 Everything is regional
715
-
716
- API Gateway WebSocket endpoints are regional. DynamoDB is regional.
717
- EventBridge Scheduler is regional. Lambda is regional. The stack
718
- deploys into exactly one region (the one your credentials resolve to
719
- via `CDK_DEFAULT_REGION`).
720
-
721
- WebSocket clients pay round-trip time to that region. A user in
722
- Frankfurt connecting to a `us-east-1` deployment will see ~90 ms RTT
723
- on every PRIVMSG roundtrip; the same user against `eu-central-1` will
724
- see ~10 ms. **Pick the region closest to your user base.**
725
-
726
- ### 10.2 Recommendation: single-region for v1
727
-
728
- For v1, deploy in one region. There are no static assets (CloudFront
729
- and S3 are not part of this stack), so a CDN adds no value here.
730
- Single-region keeps the data model simple: the cross-table
731
- `TransactWriteItems` flows (§9.2) only work within one region.
732
-
733
- ### 10.3 Multi-region is out of scope for v1
734
-
735
- If you ever need multi-region active-active, the considerations are:
736
-
737
- - **DynamoDB global tables** — replicate all five tables. Eventual
738
- consistency across regions complicates the conditional-write
739
- invariants (nick uniqueness, membership transactions): a nick
740
- reserved in region A may not be visible in region B for a second or
741
- two, allowing duplicate reservations. The reducer logic would need
742
- to tolerate this.
743
- - **Per-region APIGW + Lambda** — each region runs the full stack;
744
- clients are routed by DNS (Route 53 latency-based routing).
745
- - **Session affinity** — a client's `connectionId` is regional. A
746
- reconnect after the 2-hour APIGW cap must land on the same region
747
- or treat the new connection as a fresh registration (re-NICK,
748
- re-JOIN).
749
-
750
- This is post-v1 work; the AWS adapter architecture doc calls it out
751
- as an open question.
752
-
753
- ### 10.4 Region pick shortlist
754
-
755
- For a globally distributed user base, the typical picks:
756
-
757
- - **US-East-1 (N. Virginia)** — newest services, cheapest, ~100 ms to
758
- EU and ~150 ms to APAC. Default for the CI workflow's `AWS_REGION`.
759
- - **EU-West-1 (Ireland)** / **EU-Central-1 (Frankfurt)** — best for
760
- EU users.
761
- - **AP-Northeast-1 (Tokyo)** / **AP-Southeast-1 (Singapore)** — best
762
- for APAC users.
763
-
764
- ---
765
-
766
- ## 11. Cost notes
767
-
768
- All figures are the public AWS list price as of late 2024 — **always
769
- verify against the current AWS pricing page** before budgeting:
770
-
771
- - API Gateway WebSocket: <https://aws.amazon.com/apigateway/pricing/>
772
- - Lambda: <https://aws.amazon.com/lambda/pricing/>
773
- - DynamoDB: <https://aws.amazon.com/dynamodb/pricing/>
774
- - EventBridge: <https://aws.amazon.com/eventbridge/pricing/>
775
-
776
- ### 11.1 Billable services
777
-
778
- | Service | Unit | List price (late 2024) |
779
- |--------------------------|------------------------------------------------|-------------------------------|
780
- | API Gateway WebSocket | connection-minute | $0.80 / million |
781
- | API Gateway WebSocket | message (inbound or outbound) | $1.00 / million |
782
- | Lambda | request | $0.20 / million |
783
- | Lambda | GB-second | $0.0000166667 (≈ $0.0167 / GB-hour) |
784
- | DynamoDB on-demand | write request unit (WRU) | $1.25 / million |
785
- | DynamoDB on-demand | read request unit (RRU) | $0.25 / million |
786
- | EventBridge Scheduler | event published | $1.00 / million |
787
- | CloudWatch Logs | GB ingested | $0.50 |
788
-
789
- The Lambda free tier (1M requests + 400k GB-seconds per month) covers
790
- a meaningful slice of a small staging deployment.
791
-
792
- ### 11.2 Back-of-envelope: 100 concurrent users
793
-
794
- Assume a small staging deployment with 100 concurrent users, average
795
- 5 messages/sec across all channels, each connection active for 1 hour,
796
- average Lambda invocation 50 ms at 128 MB:
797
-
798
- | Dimension | Estimate | Monthly cost (approx) |
799
- |------------------------------------------|---------------------------------------|-----------------------|
800
- | APIGW connection-minutes | 100 conn × 60 min × 24 h × 30 d = 4.32M conn-min | ~$3.50 |
801
- | APIGW messages (inbound + fanout) | 5 msg/s × 86400 × 30 = 13M inbound, ~3× fanout | ~$50 |
802
- | Lambda invocations | ~1 per inbound + fanout ≈ 40M | ~$8.00 (free tier offsets some) |
803
- | Lambda GB-seconds | 40M × 0.05 s × 0.125 GB = 250k GB-s | ~$4.20 |
804
- | DynamoDB writes (Connections updates + ChannelMembers deltas) | ~5M WRU | ~$6.25 |
805
- | DynamoDB reads (fanout Queries + Connections GetItem) | ~30M RRU | ~$7.50 |
806
- | EventBridge events (sweeper + pingChecker) | 5-min + 1-min ticks ≈ 50k events | <$0.10 |
807
- | CloudWatch Logs | ~5 GB | ~$2.50 |
808
-
809
- **Rough total: ~$80/month** for a 100-concurrent-user staging
810
- deployment. The dominant cost is **APIGW messages** (inbound + the
811
- per-member fanout). The single biggest lever is channel size — a
812
- single 1k-member announcement channel can multiply the fanout cost by
813
- 10×.
814
-
815
- ### 11.3 Cost-trim levers
816
-
817
- - **Cap channel size** with `+l` mode (the existing MODE enforcement).
818
- - **Consider provisioned capacity** for `ChannelMembers` once traffic
819
- is steady (§9.7).
820
- - **Shorten the sweeper cadence** if you can tolerate slower cleanup —
821
- `rate(5 minutes)` is a balance; `rate(15 minutes)` cuts sweeper
822
- cost 3× at the cost of slower gone-connection reaping.
823
- - **Tune Lambda memory** — 128 MB is the default and cheapest per
824
- invocation, but a higher memory allocation can reduce duration
825
- enough to win on GB-seconds.
826
-
827
- ---
828
-
829
- ## 12. CI/CD
830
-
831
- The GitHub Actions workflow at `.github/workflows/deploy-aws.yml`
832
- deploys staging on every push to `main` and replays the smoke e2e. It
833
- is the canonical deploy path — manual `pnpm deploy:aws:staging` is for
834
- iteration only.
835
-
836
- ### 12.1 What the workflow does
837
-
838
- Steps performed (in order):
839
-
840
- 1. Checkout.
841
- 2. Install pnpm 9 + Node 20 (`cache: pnpm`).
842
- 3. `pnpm install --frozen-lockfile`.
843
- 4. `pnpm build` — builds all workspace packages.
844
- 5. `pnpm typecheck` and `pnpm test` — gate the deploy.
845
- 6. Configure AWS credentials (OIDC if `AWS_DEPLOY_ROLE_ARN` is set,
846
- otherwise static keys).
847
- 7. `pnpm deploy:aws:staging` → `cdk deploy --all --require-approval never`.
848
- 8. Resolve the smoke URL: repo variable `AWS_SMOKE_URL` first, else
849
- the stack's `ConnectUrl` CloudFormation output via
850
- `aws cloudformation describe-stacks --stack-name IrcAwsStack`, else
851
- skip the smoke step with a warning.
852
- 9. `pnpm smoke:aws:staging -- --url "$SMOKE_URL"` — failure fails the
853
- build.
854
-
855
- Concurrency is serialized via `concurrency.group: aws-staging`,
856
- `cancel-in-progress: false`, so two pushes cannot race the same
857
- CloudFormation stack.
858
-
859
- ### 12.2 Required repo configuration
860
-
861
- Under **Settings → Secrets and variables → Actions**:
862
-
863
- - **Secret `AWS_ACCESS_KEY_ID`** — required unless OIDC.
864
- - **Secret `AWS_SECRET_ACCESS_KEY`** — required unless OIDC.
865
- - **Secret `AWS_DEPLOY_ROLE_ARN`** — optional; enables OIDC.
866
- - **Secret `AWS_REGION`** — required (e.g. `us-east-1`).
867
- - **Variable `AWS_SMOKE_URL`** — optional but recommended; the
868
- workflow can fall back to the CFN output, but setting this variable
869
- explicitly avoids the `describe-stacks` round-trip and the
870
- possibility of a stale cached value after a redeploy.
871
-
872
- ### 12.3 OIDC vs static keys
873
-
874
- The workflow supports both auth modes:
875
-
876
- ```yaml
877
- - name: Configure AWS credentials
878
- uses: aws-actions/configure-aws-credentials@v4
879
- with:
880
- aws-region: ${{ secrets.AWS_REGION }}
881
- role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
882
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
883
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
884
- ```
885
-
886
- When `AWS_DEPLOY_ROLE_ARN` is set, the action assumes the role via
887
- OIDC web identity (requires `permissions.id-token: write`, which the
888
- workflow declares) and ignores the static keys. When unset, it falls
889
- back to the static access-key pair. OIDC is the recommended path for
890
- any deployment tied to a real AWS account — no long-lived credentials
891
- to rotate.
892
-
893
- ### 12.4 No CI auto-deploy to production
894
-
895
- Production deploys are intentionally manual (`pnpm deploy:aws:prod`
896
- from a clean checkout on `main`, against separate credentials). The
897
- workflow's `on:` block only triggers on `push` to `main` and
898
- `workflow_dispatch`, both of which target staging.
899
-
900
- ---
901
-
902
- ## 13. Troubleshooting
903
-
904
- ### 13.1 `cdk deploy` fails with bucket-not-found
905
-
906
- > `BUCKET_NAME_PARAMETER_<id> does not exist` or
907
- > `NoSuchBucket: The specified bucket does not exist`
908
-
909
- The CDK bootstrap stack hasn't been created in this account/region.
910
-
911
- ```bash
912
- cd apps/aws-stack
913
- pnpm cdk bootstrap
914
- pnpm deploy:aws:staging
915
- ```
916
-
917
- Bootstrap is idempotent and per-region — re-run it if you change
918
- regions.
919
-
920
- ### 13.2 Lambda throws on cold start because of a missing env var
921
-
922
- Symptom: smoke e2e times out waiting for `001`; CloudWatch Logs for
923
- `IrcHandler` show an `Error: Missing env var CONNECTIONS_TABLE (table
924
- name for Connections)` (or one of the other four `<NAME>_TABLE` vars).
925
-
926
- The `tablesConfigFromEnv` helper throws on the first invocation if any
927
- of `CONNECTIONS_TABLE`, `CHANNELMETA_TABLE`, `CHANNELMEMBERS_TABLE`,
928
- `NICKS_TABLE`, `ACCOUNTS_TABLE` is missing. The CDK stack always
929
- injects these — if one is missing in the console, somebody hand-edited
930
- the Lambda config. Fix by re-deploying:
931
-
932
- ```bash
933
- pnpm deploy:aws:staging
934
- ```
935
-
936
- If the error mentions `MANAGEMENT_URL` being absent: that env var is
937
- optional at the loader level (`buildDepsFromEnv` tolerates its absence
938
- by setting `managementApi = null`), but the handler's fanout path will
939
- fail at the first `postToConnection`. Verify both `IrcHandler` and
940
- `IrcPingChecker` carry `MANAGEMENT_URL` in the console.
941
-
942
- ### 13.3 Smoke e2e times out waiting for `001`
943
-
944
- The connection upgrade succeeded (the WebSocket `open` fired) but
945
- registration didn't complete. Common causes:
946
-
947
- - **The handler threw on the first frame.** Tail the logs:
948
- ```bash
949
- aws logs tail /aws/lambda/IrcHandler --follow
950
- ```
951
- and reproduce. Look for the cold-start `buildDepsFromEnv` throw
952
- (§13.2) or an unexpected exception in `handleDefault`.
953
- - **The `prod` stage didn't deploy.** The stage is `autoDeploy: true`,
954
- so this is rare, but verify the URL ends in `/prod` and that the
955
- route is wired:
956
- ```bash
957
- aws apigatewayv2 get-routes --api-id <api-id>
958
- ```
959
- You should see three routes: `$connect`, `$disconnect`, `$default`.
960
- - **Client connected over plain `ws://`.** APIGW WebSocket stages only
961
- accept `wss://`. Use the URL printed by `cdk deploy` (the
962
- `ConnectUrl` output).
963
-
964
- ### 13.4 `aws cloudformation describe-stacks` returns nothing in CI
965
-
966
- The workflow uses:
967
-
968
- ```bash
969
- aws cloudformation describe-stacks \
970
- --stack-name IrcAwsStack \
971
- --query 'Stacks[0].Outputs[?OutputKey==`ConnectUrl`].OutputValue' \
972
- --output text
973
- ```
974
-
975
- to resolve the smoke URL. If this returns nothing, either:
976
-
977
- - **The stack name is wrong.** It's hardcoded as `IrcAwsStack` in
978
- `apps/aws-stack/bin/aws.ts`. If you forked the stack to a different
979
- name, the workflow's `--stack-name IrcAwsStack` won't find it.
980
- Update the workflow or rename back.
981
- - **The deploy failed silently.** Check the prior `pnpm
982
- deploy:aws:staging` step's exit code. `cdk deploy` with
983
- `--require-approval never` can still fail on IAM or
984
- CloudFormation errors.
985
- - **Wrong region.** The `describe-stacks` call uses `AWS_REGION` from
986
- the secrets; it must match the region the stack was deployed into.
987
-
988
- The workflow prints a `::warning::` and skips the smoke step rather
989
- than failing the build — a silent skip is the failure mode to watch
990
- for.
991
-
992
- ### 13.5 WebSocket client can't connect
993
-
994
- The `$connect` route is wired to `IrcHandler` and currently uses **no
995
- auth** (no authorizer). If the client gets a 4xx/5xx on the HTTP
996
- upgrade:
997
-
998
- - Verify the URL is the `ConnectUrl` output, not `ManagementUrl` (the
999
- management endpoint rejects WebSocket upgrades).
1000
- - Verify the `IrcHandler` Lambda has `execute-api:ManageConnections`
1001
- on this stage (the stack grants this via
1002
- `stage.grantManagementApiAccess(handler)`).
1003
- - If you add an authorizer or IP allowlist later, the `$connect`
1004
- Lambda is the natural place to gate by source IP (read
1005
- `event.requestContext.connectionId` and
1006
- `event.requestContext.sourceIp`).
1007
-
1008
- ### 13.6 DynamoDB table vanished after `cdk destroy`
1009
-
1010
- By design. The stack uses `RemovalPolicy.DESTROY` on every table
1011
- (§9.4) so staging teardowns are clean. **Do not run
1012
- `cdk destroy` against a production stack.** For production, fork the
1013
- stack to `RemovalPolicy.RETAIN` before the first deploy; flagged in
1014
- §14.
1015
-
1016
- ### 13.7 High DynamoDB cost on `Connections`
1017
-
1018
- The gone-connection sweeper `Scan`s the entire `Connections` table
1019
- every 5 minutes (§9.6). At very large scale this dominates the read
1020
- bill. Short-term mitigation: increase the sweeper's `rate(...)` to
1021
- `rate(15 minutes)` or `rate(30 minutes)` in `aws-stack.ts`. Long-term:
1022
- add a sparse GSI keyed by `idleSince` and switch the sweeper to a
1023
- `Query` against it (post-v1 work).
1024
-
1025
- ### 13.8 Nick reservation always fails
1026
-
1027
- A client's `NICK` always returns `433 ERR_NICKNAMEINUSE`, even for
1028
- nicks that should be free. Two deployments sharing the same DynamoDB
1029
- table name space is the usual cause — e.g. you deployed `IrcAwsStack`
1030
- twice into the same account/region with a forked stack name. The
1031
- `Nicks` table name is hardcoded as `Nicks` (physical name), so two
1032
- stacks in the same region collide on the same table. Use separate
1033
- regions or fork the table-name prefix.
1034
-
1035
- ---
1036
-
1037
- ## 14. Quick reference
1038
-
1039
- ```bash
1040
- # One-time account setup (per region)
1041
- aws configure # or: export AWS_PROFILE=...
1042
- cd apps/aws-stack && pnpm cdk bootstrap
1043
-
1044
- # Local dev
1045
- pnpm install
1046
- pnpm build
1047
- pnpm --filter @serverless-ircd/aws-stack test # synth-time assertions
1048
- pnpm --filter @serverless-ircd/aws-stack cdk:synth # writes cdk.out/
1049
-
1050
- # Optional localstack validation (Docker required)
1051
- docker run --rm -d -p 4566:4566 localstack/localstack
1052
- npm install -g aws-cdk-local
1053
- export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1
1054
- LOCALSTACK=1 pnpm --filter @serverless-ircd/aws-stack test
1055
-
1056
- # Staging
1057
- pnpm deploy:aws:staging
1058
- node apps/aws-stack/scripts/smoke.mjs \
1059
- --url wss://<id>.execute-api.<region>.amazonaws.com/prod
1060
- aws logs tail /aws/lambda/IrcHandler --follow # live logs
1061
-
1062
- # Production (manual, separate credentials)
1063
- pnpm deploy:aws:prod
1064
-
1065
- # Tests / lint
1066
- pnpm test # full workspace
1067
- pnpm --filter @serverless-ircd/aws-adapter test
1068
- pnpm --filter @serverless-ircd/aws-stack test
1069
- pnpm lint
1070
- ```
1071
-
1072
- Key files:
1073
-
1074
- | Path | What |
1075
- |---------------------------------------------------|-----------------------------------------------------|
1076
- | `apps/aws-stack/src/aws-stack.ts` | CDK stack — all constructs and env-var wiring. |
1077
- | `apps/aws-stack/bin/aws.ts` | App entry. Hardcodes stack name `IrcAwsStack`. |
1078
- | `apps/aws-stack/cdk.json` | `app = tsx bin/aws.ts`. |
1079
- | `apps/aws-stack/scripts/smoke.mjs` | CI + local smoke e2e. |
1080
- | `apps/aws-stack/tests/stack.test.ts` | Synth-time assertions (cross-check for every fact). |
1081
- | `packages/aws-adapter/src/cdk-table-defs.ts` | Five DynamoDB table shapes (keys, billing, TTL). |
1082
- | `packages/aws-adapter/src/tables.ts` | Runtime table-name + key-column constants. |
1083
- | `packages/aws-adapter/src/config-loader.ts` | Lambda env → `ServerConfig` schema mapping. |
1084
- | `packages/aws-adapter/src/handlers/index.ts` | Lambda entry points (`handler`, `sweeperHandler`, `pingCheckerHandler`). |
1085
- | `.github/workflows/deploy-aws.yml` | Staging deploy + smoke e2e CI. |
1086
-
1087
- ### Known gaps flagged for follow-up
1088
-
1089
- These are facts the **code does not yet have an answer for**; the
1090
- recommendations above are deployer guidance, not built-in knobs:
1091
-
1092
- 1. **`RemovalPolicy.DESTROY` is hardcoded.** There is no stack prop to
1093
- flip it to `RETAIN` for production. Recommendation: a future change
1094
- adds a `production: boolean` (or similar) construct prop that
1095
- toggles the removal policy across all five tables.
1096
- 2. ~~**`SERVER_NAME` / `NETWORK_NAME` / `MOTD` are hardcoded in
1097
- `aws-stack.ts`.**~~ **Resolved** — these are now CDK construct props
1098
- (`serverName`, `networkName`, `motdLines`) on `IrcStackProps` (see
1099
- §7.1).
1100
- 3. ~~**MOTD multiline parsing.**~~ **Resolved** — the stack's
1101
- `motdLines` prop is a `string[]` joined with `\n`, which the config
1102
- loader splits back into one line per entry. The delimiter is `\n`
1103
- only; commas are treated as literal line content (see §7.1).
1104
- 4. **No per-environment stack parameter.** Staging and production
1105
- cannot coexist in the same account/region without forking the
1106
- stack to parameterize names. Recommendation: separate accounts for
1107
- staging vs production for v1.