serverless-ircd 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/.github/workflows/ci.yml +28 -0
  2. package/.github/workflows/deploy-aws.yml +156 -32
  3. package/.github/workflows/deploy-cf-tcp.yml +35 -9
  4. package/.github/workflows/deploy-cf.yml +40 -14
  5. package/CHANGELOG.md +594 -0
  6. package/README.md +286 -60
  7. package/apps/aws-stack/README.md +3 -5
  8. package/apps/aws-stack/bin/aws.ts +118 -9
  9. package/apps/aws-stack/cdk.json +0 -3
  10. package/apps/aws-stack/package.json +3 -4
  11. package/apps/aws-stack/src/aws-stack.ts +398 -67
  12. package/apps/aws-stack/src/static-site.ts +323 -0
  13. package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
  14. package/apps/aws-stack/tests/stack.test.ts +714 -105
  15. package/apps/aws-stack/tests/static-site.test.ts +491 -0
  16. package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
  17. package/apps/cf-tcp-container/Dockerfile +37 -5
  18. package/apps/cf-tcp-container/package.json +7 -3
  19. package/apps/cf-tcp-container/src/config-loader.ts +113 -2
  20. package/apps/cf-tcp-container/src/container-server.ts +267 -87
  21. package/apps/cf-tcp-container/src/main.ts +22 -7
  22. package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
  25. package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
  26. package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
  27. package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
  28. package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
  29. package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
  30. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
  31. package/apps/cf-tcp-container/wrangler.toml +18 -14
  32. package/apps/cf-worker/package.json +3 -4
  33. package/apps/cf-worker/src/worker.ts +77 -5
  34. package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
  35. package/apps/cf-worker/tests/smoke.test.ts +4 -0
  36. package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
  37. package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
  38. package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
  39. package/apps/cf-worker/wrangler.test.toml +15 -1
  40. package/apps/cf-worker/wrangler.toml +95 -77
  41. package/apps/local-cli/package.json +1 -1
  42. package/apps/local-cli/src/config-loader.ts +14 -2
  43. package/apps/local-cli/src/line-scanner.ts +26 -0
  44. package/apps/local-cli/src/server.ts +44 -19
  45. package/apps/local-cli/tests/line-scanner.test.ts +64 -0
  46. package/apps/local-cli/tests/tcp.test.ts +29 -0
  47. package/apps/web/landing/favicon.ico +0 -0
  48. package/apps/web/landing/index.html +1 -0
  49. package/apps/web/package.json +2 -2
  50. package/apps/web/scripts/build.mjs +66 -4
  51. package/apps/web/src/build-env.ts +125 -4
  52. package/apps/web/src/config-schema.ts +20 -6
  53. package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
  54. package/apps/web/tests/build-env.test.ts +210 -9
  55. package/apps/web/tests/build-smoke.test.ts +2 -2
  56. package/apps/web/tests/config-schema.test.ts +149 -25
  57. package/docs/AWS-Deployment.md +793 -118
  58. package/docs/AWS-TCP-Deployment.md +57 -47
  59. package/docs/Chat-History.md +55 -0
  60. package/docs/Cloudflare-Deployment-Guide.md +95 -114
  61. package/docs/Cloudflare-TCP-Deployment.md +160 -101
  62. package/docs/Release-Process.md +27 -23
  63. package/docs/SASL-EXTERNAL.md +175 -0
  64. package/docs/Services.md +69 -22
  65. package/docs/WebClientGuide.md +35 -26
  66. package/package.json +7 -10
  67. package/packages/aws-adapter/package.json +1 -1
  68. package/packages/aws-adapter/src/admission.ts +28 -13
  69. package/packages/aws-adapter/src/aws-runtime.ts +30 -3
  70. package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
  71. package/packages/aws-adapter/src/config-loader.ts +153 -8
  72. package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
  73. package/packages/aws-adapter/src/handlers/connect.ts +73 -1
  74. package/packages/aws-adapter/src/handlers/default.ts +279 -123
  75. package/packages/aws-adapter/src/handlers/index.ts +98 -25
  76. package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
  77. package/packages/aws-adapter/src/index.ts +5 -7
  78. package/packages/aws-adapter/src/ip-admission.ts +79 -0
  79. package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
  80. package/packages/aws-adapter/src/serialize.ts +23 -0
  81. package/packages/aws-adapter/src/tables.ts +11 -12
  82. package/packages/aws-adapter/tests/admission.test.ts +60 -2
  83. package/packages/aws-adapter/tests/aws-harness.ts +23 -2
  84. package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
  85. package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
  86. package/packages/aws-adapter/tests/connect.test.ts +323 -3
  87. package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
  88. package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
  89. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
  90. package/packages/aws-adapter/tests/handlers.test.ts +174 -12
  91. package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
  92. package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
  93. package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
  94. package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
  95. package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
  96. package/packages/aws-adapter/tests/stats.test.ts +0 -3
  97. package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
  98. package/packages/aws-adapter/tests/tables.test.ts +1 -8
  99. package/packages/aws-adapter/tests/transactions.test.ts +0 -1
  100. package/packages/cf-adapter/package.json +1 -5
  101. package/packages/cf-adapter/src/cf-runtime.ts +100 -10
  102. package/packages/cf-adapter/src/channel-do.ts +13 -3
  103. package/packages/cf-adapter/src/config-loader.ts +133 -8
  104. package/packages/cf-adapter/src/connection-do.ts +406 -116
  105. package/packages/cf-adapter/src/counter-do.ts +142 -0
  106. package/packages/cf-adapter/src/d1-services-store.ts +105 -26
  107. package/packages/cf-adapter/src/env.ts +99 -10
  108. package/packages/cf-adapter/src/index.ts +17 -7
  109. package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
  110. package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
  111. package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
  112. package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
  113. package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
  114. package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
  115. package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
  116. package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
  117. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
  118. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
  119. package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
  120. package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
  121. package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
  122. package/packages/cf-adapter/tests/serialize.test.ts +25 -0
  123. package/packages/cf-adapter/tests/worker/main.ts +4 -0
  124. package/packages/cf-adapter/wrangler.test.toml +18 -1
  125. package/packages/in-memory-runtime/package.json +1 -1
  126. package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
  127. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
  128. package/packages/irc-core/package.json +1 -1
  129. package/packages/irc-core/src/account-migration.ts +140 -0
  130. package/packages/irc-core/src/caps/capabilities.ts +20 -10
  131. package/packages/irc-core/src/certfp.ts +178 -0
  132. package/packages/irc-core/src/commands/account-auth.ts +16 -19
  133. package/packages/irc-core/src/commands/cap.ts +10 -2
  134. package/packages/irc-core/src/commands/chanserv.ts +117 -14
  135. package/packages/irc-core/src/commands/chathistory.ts +13 -5
  136. package/packages/irc-core/src/commands/hostserv.ts +84 -8
  137. package/packages/irc-core/src/commands/index.ts +2 -1
  138. package/packages/irc-core/src/commands/invite.ts +1 -7
  139. package/packages/irc-core/src/commands/join.ts +1 -16
  140. package/packages/irc-core/src/commands/kick.ts +1 -8
  141. package/packages/irc-core/src/commands/list.ts +1 -8
  142. package/packages/irc-core/src/commands/memoserv.ts +1 -1
  143. package/packages/irc-core/src/commands/mode.ts +1 -8
  144. package/packages/irc-core/src/commands/multiline.ts +4 -10
  145. package/packages/irc-core/src/commands/names.ts +53 -13
  146. package/packages/irc-core/src/commands/nickserv.ts +161 -11
  147. package/packages/irc-core/src/commands/oper.ts +361 -8
  148. package/packages/irc-core/src/commands/part.ts +4 -10
  149. package/packages/irc-core/src/commands/privmsg.ts +8 -4
  150. package/packages/irc-core/src/commands/registration.ts +148 -4
  151. package/packages/irc-core/src/commands/sasl.ts +154 -46
  152. package/packages/irc-core/src/commands/topic.ts +10 -12
  153. package/packages/irc-core/src/commands/who.ts +1 -8
  154. package/packages/irc-core/src/config.ts +424 -25
  155. package/packages/irc-core/src/credential-hashing.ts +11 -54
  156. package/packages/irc-core/src/effects.ts +24 -0
  157. package/packages/irc-core/src/flood-control.ts +10 -10
  158. package/packages/irc-core/src/frame-rate-limit.ts +82 -0
  159. package/packages/irc-core/src/index.ts +9 -0
  160. package/packages/irc-core/src/oper-hashing.ts +43 -0
  161. package/packages/irc-core/src/oper-lockout.ts +87 -0
  162. package/packages/irc-core/src/ports.ts +529 -190
  163. package/packages/irc-core/src/protocol/bytes.ts +65 -0
  164. package/packages/irc-core/src/protocol/channel-name.ts +37 -0
  165. package/packages/irc-core/src/protocol/index.ts +12 -1
  166. package/packages/irc-core/src/protocol/outbound.ts +43 -10
  167. package/packages/irc-core/src/protocol/parser.ts +79 -10
  168. package/packages/irc-core/src/state/connection.ts +13 -0
  169. package/packages/irc-core/src/types.ts +266 -23
  170. package/packages/irc-core/src/ws-framing.ts +5 -4
  171. package/packages/irc-core/tests/account-migration.test.ts +133 -0
  172. package/packages/irc-core/tests/bytes.test.ts +89 -0
  173. package/packages/irc-core/tests/certfp.test.ts +117 -0
  174. package/packages/irc-core/tests/commands/cap.test.ts +76 -2
  175. package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
  176. package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
  177. package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
  178. package/packages/irc-core/tests/commands/join.test.ts +78 -1
  179. package/packages/irc-core/tests/commands/markread.test.ts +54 -0
  180. package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
  181. package/packages/irc-core/tests/commands/names.test.ts +193 -0
  182. package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
  183. package/packages/irc-core/tests/commands/oper.test.ts +574 -1
  184. package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
  185. package/packages/irc-core/tests/commands/registration.test.ts +602 -133
  186. package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
  187. package/packages/irc-core/tests/commands/topic.test.ts +137 -2
  188. package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
  189. package/packages/irc-core/tests/config.test.ts +534 -2
  190. package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
  191. package/packages/irc-core/tests/effects.test.ts +14 -0
  192. package/packages/irc-core/tests/flood-control.test.ts +29 -1
  193. package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
  194. package/packages/irc-core/tests/message-store.test.ts +5 -0
  195. package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
  196. package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
  197. package/packages/irc-core/tests/outbound.test.ts +148 -0
  198. package/packages/irc-core/tests/parser.test.ts +287 -5
  199. package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
  200. package/packages/irc-core/tests/ports.test.ts +170 -7
  201. package/packages/irc-core/tests/services-store.test.ts +567 -1
  202. package/packages/irc-core/tests/ws-framing.test.ts +45 -0
  203. package/packages/irc-core/vitest.config.ts +6 -1
  204. package/packages/irc-server/package.json +1 -1
  205. package/packages/irc-server/src/actor.ts +123 -22
  206. package/packages/irc-server/src/dispatch.ts +1 -0
  207. package/packages/irc-server/src/index.ts +7 -0
  208. package/packages/irc-server/src/redact.ts +159 -0
  209. package/packages/irc-server/src/runtime.ts +14 -0
  210. package/packages/irc-server/src/transport.ts +28 -1
  211. package/packages/irc-server/tests/actor.test.ts +563 -54
  212. package/packages/irc-server/tests/dispatch.test.ts +31 -0
  213. package/packages/irc-server/tests/redact.test.ts +198 -0
  214. package/packages/irc-server/tests/runtime.test.ts +2 -0
  215. package/packages/irc-server/tests/transport.test.ts +66 -0
  216. package/packages/irc-test-support/package.json +1 -1
  217. package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
  218. package/pnpm-workspace.yaml +1 -0
  219. package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
  220. package/scripts/deploy-web-aws.mjs +290 -0
  221. package/scripts/package.json +23 -0
  222. package/scripts/tsconfig.test.json +12 -0
  223. package/scripts/vitest.config.ts +19 -0
  224. package/tools/ci-hardening/package.json +2 -2
  225. package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
  226. package/tools/ci-hardening/src/cf-deploy.ts +118 -0
  227. package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
  228. package/tools/ci-hardening/src/env-var-drift.ts +192 -0
  229. package/tools/ci-hardening/src/hostname-guard.ts +11 -0
  230. package/tools/ci-hardening/src/index.ts +19 -0
  231. package/tools/ci-hardening/src/validate.ts +57 -0
  232. package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
  233. package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
  234. package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
  235. package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
  236. package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
  237. package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
  238. package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
  239. package/tools/ci-hardening/tests/validate.test.ts +42 -0
  240. package/tools/ci-hardening/vitest.config.ts +5 -1
  241. package/tools/hash-oper-cred.ts +85 -0
  242. package/tools/load-test/package.json +1 -1
  243. package/tools/migrate-accounts-to-services.ts +270 -0
  244. package/tools/package.json +2 -1
  245. package/tools/seed-aws-accounts.ts +35 -10
  246. package/tools/seed-cf-accounts.ts +42 -9
  247. package/tools/tcp-ws-forwarder/package.json +1 -1
  248. package/packages/aws-adapter/src/account-store.ts +0 -121
  249. package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
  250. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
  251. package/packages/aws-adapter/tests/account-store.test.ts +0 -276
  252. package/packages/cf-adapter/src/d1-account-store.ts +0 -198
  253. package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
  254. package/packages/irc-core/tests/account-store.test.ts +0 -131
@@ -124,6 +124,34 @@ jobs:
124
124
  # config) before it reaches main.
125
125
  run: pnpm --filter @serverless-ircd/ci-hardening test
126
126
 
127
+ - name: Container non-root smoke (gated on docker availability)
128
+ # Live proof that the cf-tcp-container image drops root: the
129
+ # runtime UID must be 1000 and /data must be writable by it.
130
+ # The static Dockerfile assertions in
131
+ # apps/cf-tcp-container/tests/dockerfile.test.ts always run in
132
+ # the test step; this smoke additionally builds and runs the
133
+ # real image. Gated because the self-hosted runner image ships
134
+ # without a docker daemon (see the DynamoDB Local notes above).
135
+ run: |
136
+ if ! command -v docker >/dev/null 2>&1 || ! timeout 15 docker info >/dev/null 2>&1; then
137
+ echo "docker not available — skipping live container smoke (static Dockerfile test covers it)"
138
+ exit 0
139
+ fi
140
+ docker build -f apps/cf-tcp-container/Dockerfile -t sirc-tcp-origin-smoke .
141
+ uid="$(docker run --rm sirc-tcp-origin-smoke id -u)"
142
+ echo "runtime UID: ${uid}"
143
+ if [ "${uid}" != "1000" ]; then
144
+ echo "FAIL: expected runtime UID 1000, got ${uid}"
145
+ exit 1
146
+ fi
147
+ docker run --rm sirc-tcp-origin-smoke \
148
+ node -e "const fs=require('fs');fs.writeFileSync('/data/.smoke','ok');fs.unlinkSync('/data/.smoke')"
149
+ if [ "$?" -ne 0 ]; then
150
+ echo "FAIL: /data not writable as UID 1000"
151
+ exit 1
152
+ fi
153
+ echo "SMOKE PASS: image runs as UID 1000 and /data is writable"
154
+
127
155
  - name: Upload coverage
128
156
  if: always()
129
157
  uses: actions/upload-artifact@v3
@@ -2,30 +2,31 @@ name: deploy-aws
2
2
 
3
3
  # AWS CDK deploy pipeline.
4
4
  #
5
- # On every push to `main`:
5
+ # This workflow is `workflow_dispatch`-only — there is no automatic
6
+ # push-to-main trigger. A maintainer runs it by hand to ship the single
7
+ # AWS stack target (`apps/aws-stack`):
6
8
  # 1. Install deps (frozen lockfile).
7
9
  # 2. Build the aws-adapter + irc-core/irc-server so the CDK stack
8
10
  # typechecks against real workspace packages.
9
11
  # 3. `cdk deploy --all` the `apps/aws-stack` stack (API Gateway v2
10
- # WebSocket API, Lambda, DynamoDB tables) to the staging AWS
11
- # account/region.
12
+ # WebSocket API, Lambda, DynamoDB tables).
12
13
  # 4. Replay the smoke e2e (`scripts/smoke.mjs`) against the deployed
13
14
  # WebSocket endpoint — CONNECT/REGISTER/JOIN/PRIVMSG/QUIT. Failure
14
15
  # blocks the merge (the deploy job is required on `main`).
15
16
  #
16
17
  # Secrets / variables (configure under repo Settings → Secrets and
17
18
  # variables → Actions):
18
- # AWS_ACCESS_KEY_ID Static AWS access key with permission to deploy
19
- # the stack (IAM, APIGW, Lambda, DynamoDB).
20
- # Required unless OIDC is used (see below).
21
- # AWS_SECRET_ACCESS_KEY Companion secret key. Required unless OIDC.
22
- # AWS_DEPLOY_ROLE_ARN Optional. When set, the job assumes this role
23
- # via OIDC web identity instead of using static
24
- # access keys. Requires `permissions.id-token:
25
- # write` (declared below) and an OIDC trust
26
- # policy on the role granting
27
- # `sts:AssumeRoleWithWebIdentity` to this repo's
28
- # GitHub OIDC provider.
19
+ # AWS_DEPLOY_ROLE_ARN REQUIRED. ARN of the IAM role the workflow
20
+ # assumes via GitHub OIDC web identity. The
21
+ # role's trust policy MUST condition on
22
+ # `token.actions.githubusercontent.com:sub`
23
+ # = `repo:<owner>/<repo>:ref:refs/heads/main`
24
+ # (see docs/AWS-Deployment.md §17 for the
25
+ # trust-policy snippet). Long-lived access
26
+ # keys (`AWS_ACCESS_KEY_ID` /
27
+ # `AWS_SECRET_ACCESS_KEY`) are NOT accepted
28
+ # a single exfil vector against them is full
29
+ # AWS account takeover.
29
30
  # AWS_REGION AWS region to deploy into (e.g. us-east-1).
30
31
  #
31
32
  # Repo variable (Settings → Secrets and variables → Variables):
@@ -38,22 +39,23 @@ name: deploy-aws
38
39
  # behaviour).
39
40
 
40
41
  on:
41
- # Automatic push trigger disabled; deploy only runs manually.
42
42
  workflow_dispatch:
43
43
 
44
- # OIDC web identity requires id-token write; contents:read for checkout.
44
+ # GitHub OIDC web identity: id-token write so Actions can mint a JWT
45
+ # for `sts:AssumeRoleWithWebIdentity`; contents:read for checkout.
46
+ # Least-privilege — no other GITHUB_TOKEN permissions granted.
45
47
  permissions:
46
48
  id-token: write
47
49
  contents: read
48
50
 
49
51
  # Prevent two deploys racing the same CloudFormation stack.
50
52
  concurrency:
51
- group: aws-staging
53
+ group: aws-deploy
52
54
  cancel-in-progress: false
53
55
 
54
56
  jobs:
55
- deploy-staging:
56
- name: cdk deploy --all (staging)
57
+ deploy:
58
+ name: cdk deploy --all
57
59
  runs-on: ubuntu-latest
58
60
  timeout-minutes: 10
59
61
  steps:
@@ -81,20 +83,55 @@ jobs:
81
83
  - name: Test
82
84
  run: pnpm test
83
85
 
84
- # When AWS_DEPLOY_ROLE_ARN is set, assume it via OIDC web identity
85
- # (ignores the static access keys). Otherwise fall back to the
86
- # static access-key pair. Both shapes are handled by the action:
87
- # an empty role-to-assume is treated as unset.
86
+ # Fail fast before any AWS call if the OIDC role ARN is missing.
87
+ # `AWS_DEPLOY_ROLE_ARN` is the ONLY accepted credential source
88
+ # long-lived access keys are intentionally not wired in.
89
+ - name: Assert OIDC deploy role ARN is set
90
+ if: env.AWS_DEPLOY_ROLE_ARN == ''
91
+ env:
92
+ AWS_DEPLOY_ROLE_ARN: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
93
+ run: |
94
+ echo "::error::AWS_DEPLOY_ROLE_ARN secret is empty — configure the GitHub OIDC IAM role trust before deploying (see docs/AWS-Deployment.md §17)."
95
+ exit 1
96
+
97
+ # OIDC-only: assume the configured role via web identity. No
98
+ # static access keys are accepted; if the role ARN is empty the
99
+ # step above has already failed the job.
88
100
  - name: Configure AWS credentials
89
101
  uses: aws-actions/configure-aws-credentials@v4
90
102
  with:
91
103
  aws-region: ${{ secrets.AWS_REGION }}
92
104
  role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
93
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
94
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
95
105
 
96
- - name: Deploy to staging
97
- run: pnpm deploy:aws:staging -- -c environmentName=staging
106
+ # Defence-in-depth: confirm sts:AssumeRoleWithWebIdentity actually
107
+ # minted the configured role and the job is NOT running on
108
+ # ambient static credentials. Aborts before any `cdk deploy`.
109
+ - name: Assert assumed role matches AWS_DEPLOY_ROLE_ARN
110
+ env:
111
+ AWS_DEPLOY_ROLE_ARN: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
112
+ run: |
113
+ set -euo pipefail
114
+ CALLER_ARN=$(aws sts get-caller-identity --query 'Arn' --output text)
115
+ ASSUMED_ROLE_ARN="${AWS_DEPLOY_ROLE_ARN}"
116
+ # The caller ARN for an OIDC-assumed role looks like
117
+ # arn:aws:sts::<account>:assumed-role/<role-name>/GitHubActions
118
+ # while AWS_DEPLOY_ROLE_ARN is the role ARN
119
+ # arn:aws:iam::<account>:role/<role-name>
120
+ # Compare by role-name + account-id so the two shapes match.
121
+ CALLER_ROLE=$(echo "$CALLER_ARN" | sed -n 's#.*assumed-role/\([^/]*\).*#\1#p')
122
+ CALLER_ACCT=$(echo "$CALLER_ARN" | sed -n 's#arn:aws:sts::\([0-9]*\).*#\1#p')
123
+ CONFIG_ROLE=$(echo "$ASSUMED_ROLE_ARN" | sed -n 's#.*/role/\([^/]*\)#\1#p')
124
+ CONFIG_ACCT=$(echo "$ASSUMED_ROLE_ARN" | sed -n 's#arn:aws:iam::\([0-9]*\).*#\1#p')
125
+ if [ -z "$CALLER_ROLE" ] || [ -z "$CONFIG_ROLE" ] \
126
+ || [ "$CALLER_ROLE" != "$CONFIG_ROLE" ] \
127
+ || [ "$CALLER_ACCT" != "$CONFIG_ACCT" ]; then
128
+ echo "::error::STS caller-identity does not match AWS_DEPLOY_ROLE_ARN (got '$CALLER_ARN', expected role '$CONFIG_ROLE' in account '$CONFIG_ACCT'). OIDC did not mint the configured role — aborting before cdk deploy."
129
+ exit 1
130
+ fi
131
+ echo "OIDC role assertion passed: $CALLER_ROLE in account $CALLER_ACCT"
132
+
133
+ - name: Deploy stack
134
+ run: pnpm deploy:aws
98
135
 
99
136
  - name: Resolve smoke URL
100
137
  id: smoke-url
@@ -105,16 +142,15 @@ jobs:
105
142
  echo "url=$AWS_SMOKE_URL" >> "$GITHUB_OUTPUT"
106
143
  else
107
144
  # Fall back to the stack's ConnectUrl CloudFormation output.
108
- # Stack name is `IrcAwsStack-<environmentName>`; staging
109
- # `IrcAwsStack-staging`.
145
+ # Stack name is `IrcAwsStack` (single target, no env suffix).
110
146
  URL=$(aws cloudformation describe-stacks \
111
- --stack-name IrcAwsStack-staging \
147
+ --stack-name IrcAwsStack \
112
148
  --query 'Stacks[0].Outputs[?OutputKey==`ConnectUrl`].OutputValue' \
113
149
  --output text 2>/dev/null || true)
114
150
  if [ -n "$URL" ]; then
115
151
  echo "url=$URL" >> "$GITHUB_OUTPUT"
116
152
  else
117
- echo "::warning::Unable to resolve the staging WebSocket URL (set the AWS_SMOKE_URL repo variable or check the stack ConnectUrl output); smoke step will be skipped."
153
+ echo "::warning::Unable to resolve the WebSocket URL (set the AWS_SMOKE_URL repo variable or check the stack ConnectUrl output); smoke step will be skipped."
118
154
  echo "url=" >> "$GITHUB_OUTPUT"
119
155
  fi
120
156
  fi
@@ -123,4 +159,92 @@ jobs:
123
159
  if: steps.smoke-url.outputs.url != ''
124
160
  env:
125
161
  SMOKE_URL: ${{ steps.smoke-url.outputs.url }}
126
- run: pnpm smoke:aws:staging -- --url "$SMOKE_URL"
162
+ run: pnpm smoke:aws -- --url "$SMOKE_URL"
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # deploy-web — AWS web client two-phase deploy (s3 sync + CloudFront
166
+ # invalidation, stack-output-driven).
167
+ #
168
+ # The SPA lives in S3 behind CloudFront (provisioned by the StaticSite
169
+ # construct in apps/aws-stack). It ships SEPARATELY from the runtime
170
+ # stack because the deploy pipeline needs the stack's CloudFormation
171
+ # outputs (the API Gateway WebSocket URL, the S3 bucket name, the
172
+ # CloudFront distribution ID) to exist BEFORE it can bake the SPA —
173
+ # `cdk deploy` therefore runs first (the `deploy` job above), then this
174
+ # job reads the outputs and:
175
+ # 1. `aws cloudformation describe-stacks --stack-name IrcAwsStack`
176
+ # reads `ConnectUrl`, `WebsiteBucketName`, `WebsiteDistributionId`.
177
+ # 2. `pnpm --filter @serverless-ircd/web run build:prod-aws -- --api-url <url>`
178
+ # bakes the SPA with the real wss URL (no hardcoded URLs in the repo).
179
+ # 3. `aws s3 sync apps/web/dist s3://<bucket> --delete` uploads the
180
+ # build (the CLI skips unchanged objects via etag comparison).
181
+ # 4. `aws cloudfront create-invalidation --distribution-id <id> --paths /*`
182
+ # busts the edge cache.
183
+ #
184
+ # Why a script instead of CDK `BucketDeployment`: the script reads
185
+ # post-deploy outputs that do not exist at synth time, avoids
186
+ # re-synth-on-every-deploy, and is faster. See
187
+ # `scripts/deploy-web-aws.mjs` and `docs/AWS-Deployment.md`.
188
+ #
189
+ # Staleness note: T170 collapsed the staging/prod split — there is no
190
+ # `staging` branch and this job is `workflow_dispatch`-only on the
191
+ # current branch, mirroring the parent pipeline. A maintainer runs the
192
+ # workflow by hand to ship the AWS web client.
193
+ # ---------------------------------------------------------------------------
194
+ deploy-web:
195
+ name: web deploy (s3 sync + CloudFront invalidation)
196
+ needs: deploy
197
+ runs-on: ubuntu-latest
198
+ timeout-minutes: 15
199
+ steps:
200
+ - name: Checkout
201
+ uses: actions/checkout@v4
202
+ with:
203
+ # The web client build requires the Kiwi IRC upstream submodule
204
+ # and the docs/ submodule (rendered into dist/docs/ by build.mjs).
205
+ submodules: recursive
206
+
207
+ - name: Checkout submodules (worktree-safe fallback)
208
+ run: |
209
+ # `submodules: recursive` covers the normal case; this fallback
210
+ # ensures the Kiwi + docs submodules are present even when the
211
+ # checkout action's recursive flag was a no-op (older runners /
212
+ # path-filter configurations).
213
+ git submodule update --init --recursive
214
+
215
+ - name: Setup pnpm
216
+ uses: pnpm/action-setup@v4
217
+
218
+ - name: Setup Node
219
+ uses: actions/setup-node@v4
220
+ with:
221
+ node-version: 24
222
+ cache: pnpm
223
+
224
+ - name: Install dependencies
225
+ run: pnpm install --frozen-lockfile
226
+
227
+ # Fail fast before any AWS call if the OIDC role ARN is missing.
228
+ # Mirrors the guard in the `deploy` job — the `deploy-web` job
229
+ # also assumes the role via OIDC and accepts no static keys.
230
+ - name: Assert OIDC deploy role ARN is set
231
+ if: env.AWS_DEPLOY_ROLE_ARN == ''
232
+ env:
233
+ AWS_DEPLOY_ROLE_ARN: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
234
+ run: |
235
+ echo "::error::AWS_DEPLOY_ROLE_ARN secret is empty — configure the GitHub OIDC IAM role trust before deploying (see docs/AWS-Deployment.md §17)."
236
+ exit 1
237
+
238
+ - name: Configure AWS credentials
239
+ uses: aws-actions/configure-aws-credentials@v4
240
+ with:
241
+ aws-region: ${{ secrets.AWS_REGION }}
242
+ role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
243
+
244
+ # Two-phase deploy: describe-stacks → bake (--api-url) → s3 sync →
245
+ # create-invalidation. Implemented in scripts/deploy-web-aws.mjs;
246
+ # exits non-zero with a clear message if any required stack output
247
+ # is missing (i.e. the parent `deploy` job did not run first or the
248
+ # stack's `webSite` prop is not configured).
249
+ - name: Deploy web client (bake + s3 sync + CloudFront invalidation)
250
+ run: node scripts/deploy-web-aws.mjs
@@ -6,26 +6,41 @@ name: deploy-cf-tcp
6
6
  # Containers and applies the Spectrum TLS config (terraform). Spectrum
7
7
  # terminates TLS on :6697 and forwards plaintext TCP to the container.
8
8
  #
9
- # This pipeline runs alongside deploy-cf.yml (the WebSocket path). The two
10
- # are independent: a deployment can enable either or both transports.
9
+ # This pipeline is `workflow_dispatch`-only there is no automatic
10
+ # push-to-main trigger. A maintainer runs it by hand to ship the single
11
+ # TCP container target. It runs alongside deploy-cf.yml (the WebSocket
12
+ # path); the two are independent, so a deployment can enable either or
13
+ # both transports.
11
14
  #
12
15
  # Secrets (configure under repo Settings → Secrets and variables → Actions):
13
16
  # CLOUDFLARE_API_TOKEN Spectrum/Containers API token.
14
17
  # CLOUDFLARE_ACCOUNT_ID Account ID for the target zone.
15
18
  # CF_ZONE_ID Zone ID for the Spectrum hostname.
16
19
  # CF_TCP_ORIGIN_ADDRESS Hostname/IP of the container origin.
20
+ #
21
+ # Inputs (prompted on workflow_dispatch):
22
+ # hostname REQUIRED. Public IRC hostname this deploy
23
+ # serves (e.g. irc.example.net). The pipeline
24
+ # fails loudly when unset and refuses to ship
25
+ # while it is still a repo placeholder
26
+ # (irc.example.com / irc.your-domain.invalid)
27
+ # — the guard step below enforces both.
17
28
 
18
29
  on:
19
- # Automatic push trigger disabled; deploy only runs manually.
20
30
  workflow_dispatch:
31
+ inputs:
32
+ hostname:
33
+ description: 'Public IRC hostname for this deploy (must not be a placeholder)'
34
+ required: true
35
+ type: string
21
36
 
22
37
  concurrency:
23
- group: cf-tcp-staging
38
+ group: cf-tcp-deploy
24
39
  cancel-in-progress: false
25
40
 
26
41
  jobs:
27
- deploy-staging:
28
- name: deploy cf-tcp staging
42
+ deploy:
43
+ name: deploy cf-tcp
29
44
  runs-on: ubuntu-latest
30
45
  timeout-minutes: 15
31
46
  steps:
@@ -47,25 +62,36 @@ jobs:
47
62
  - name: Build workspace
48
63
  run: pnpm build
49
64
 
65
+ # Fail fast, before any deploy or terraform step, when the
66
+ # hostname input is unset or still a repo placeholder. Runs the
67
+ # same guard the deploy scripts use (tools/ci-hardening
68
+ # hostname-guard); the `::error::` message names the offending
69
+ # value and how to set a real hostname.
70
+ - name: 'Guard: reject placeholder hostnames'
71
+ env:
72
+ DEPLOY_HOSTNAME: ${{ inputs.hostname }}
73
+ run: node tools/ci-hardening/dist/hostname-guard.js "$DEPLOY_HOSTNAME"
74
+
50
75
  - name: Typecheck
51
76
  run: pnpm typecheck
52
77
 
53
78
  - name: Test
54
79
  run: pnpm --filter '@serverless-ircd/cf-tcp-container' test
55
80
 
56
- - name: Deploy container to staging
81
+ - name: Deploy container
57
82
  working-directory: apps/cf-tcp-container
58
83
  env:
59
84
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
60
85
  CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
61
- run: wrangler deploy --env staging
86
+ SERVER_NAME: ${{ inputs.hostname }}
87
+ run: pnpm run deploy -- --var "SERVER_NAME:$SERVER_NAME"
62
88
 
63
89
  - name: Apply Spectrum TLS config
64
90
  working-directory: apps/cf-tcp-container/terraform
65
91
  env:
66
92
  TF_VAR_zone_id: ${{ secrets.CF_ZONE_ID }}
67
93
  TF_VAR_origin_address: ${{ secrets.CF_TCP_ORIGIN_ADDRESS }}
68
- TF_VAR_hostname: irc-staging.example.com
94
+ TF_VAR_hostname: ${{ inputs.hostname }}
69
95
  run: |
70
96
  terraform init
71
97
  terraform apply -auto-approve
@@ -2,12 +2,13 @@ name: deploy-cf
2
2
 
3
3
  # Cloudflare Workers deploy pipeline.
4
4
  #
5
- # On every push to `main`:
5
+ # This workflow is `workflow_dispatch`-only — there is no automatic
6
+ # push-to-main trigger. A maintainer runs it by hand to ship the single
7
+ # Cloudflare Worker target (`apps/cf-worker`):
6
8
  # 1. Install deps (frozen lockfile).
7
9
  # 2. Build the cf-adapter + irc-core/irc-server so the Worker entry
8
10
  # typechecks against real workspace packages.
9
- # 3. `wrangler deploy --env staging` the `apps/cf-worker` worker to the
10
- # `serverless-ircd-staging` Worker + DO namespace.
11
+ # 3. `wrangler deploy` the `apps/cf-worker` worker.
11
12
  # 4. Replay the smoke e2e (`scripts/smoke.mjs`) against the deployed
12
13
  # URL — CONNECT/REGISTER/JOIN/PRIVMSG/QUIT. Failure blocks the
13
14
  # merge (the deploy job is required on `main`).
@@ -19,23 +20,36 @@ name: deploy-cf
19
20
  # CLOUDFLARE_ACCOUNT_ID Optional; required if the token's account
20
21
  # context is ambiguous.
21
22
  #
22
- # The smoke URL is derived from the staging Worker name
23
- # (`serverless-ircd-staging`) plus the deployer's account subdomain.
24
- # Override by setting `CF_SMOKE_URL` in the repo Variables store when
25
- # custom routes are wired.
23
+ # The smoke URL is derived from the Worker name (`serverless-ircd`)
24
+ # plus the deployer's account subdomain. Override by setting
25
+ # `CF_SMOKE_URL` in the repo Variables store when custom routes are
26
+ # wired.
27
+ #
28
+ # Inputs (prompted on workflow_dispatch):
29
+ # hostname REQUIRED. Public IRC hostname this deploy serves
30
+ # (e.g. irc.example.net). Fails loudly when unset and
31
+ # refuses to ship while it is still a repo placeholder
32
+ # (irc.example.com / irc.your-domain.invalid) — the
33
+ # guard step below enforces both, and the deploy step
34
+ # overrides the shipped wrangler.toml `SERVER_NAME`
35
+ # default with the input via `--var`.
26
36
 
27
37
  on:
28
- # Automatic push trigger disabled; deploy only runs manually.
29
38
  workflow_dispatch:
39
+ inputs:
40
+ hostname:
41
+ description: 'Public IRC hostname for this deploy (must not be a placeholder)'
42
+ required: true
43
+ type: string
30
44
 
31
45
  # Prevent two deploys racing the same DO namespace.
32
46
  concurrency:
33
- group: cf-staging
47
+ group: cf-deploy
34
48
  cancel-in-progress: false
35
49
 
36
50
  jobs:
37
- deploy-staging:
38
- name: wrangler deploy --env staging
51
+ deploy:
52
+ name: wrangler deploy
39
53
  runs-on: ubuntu-latest
40
54
  timeout-minutes: 10
41
55
  steps:
@@ -57,18 +71,30 @@ jobs:
57
71
  - name: Build workspace
58
72
  run: pnpm build
59
73
 
74
+ # Fail fast, before the deploy step, when the hostname input is
75
+ # unset or still a repo placeholder (same guard the deploy
76
+ # scripts use; tools/ci-hardening hostname-guard).
77
+ - name: 'Guard: reject placeholder hostnames'
78
+ env:
79
+ DEPLOY_HOSTNAME: ${{ inputs.hostname }}
80
+ run: node tools/ci-hardening/dist/hostname-guard.js "$DEPLOY_HOSTNAME"
81
+
60
82
  - name: Typecheck
61
83
  run: pnpm typecheck
62
84
 
63
85
  - name: Test
64
86
  run: pnpm test
65
87
 
66
- - name: Deploy to staging
88
+ - name: Deploy Worker
67
89
  working-directory: apps/cf-worker
68
90
  env:
69
91
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
70
92
  CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
71
- run: pnpm deploy:staging
93
+ SERVER_NAME: ${{ inputs.hostname }}
94
+ # The guarded cf-deploy wrapper resolves SERVER_NAME from the env
95
+ # (set above from the workflow input), runs the placeholder guard,
96
+ # and injects it into wrangler as --var SERVER_NAME:<hostname>.
97
+ run: pnpm run deploy
72
98
 
73
99
  - name: Resolve smoke URL
74
100
  id: smoke-url
@@ -82,7 +108,7 @@ jobs:
82
108
  # The subdomain is per-account; if CF_SMOKE_URL isn't set we
83
109
  # fall back to a workers.dev route that the deployer must
84
110
  # configure once via the Cloudflare dashboard.
85
- echo "::warning::CF_SMOKE_URL repo variable is not set; smoke step will be skipped. Set it to the staging Worker's wss:// URL."
111
+ echo "::warning::CF_SMOKE_URL repo variable is not set; smoke step will be skipped. Set it to the Worker's wss:// URL."
86
112
  echo "url=" >> "$GITHUB_OUTPUT"
87
113
  fi
88
114