serverless-ircd 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.github/workflows/deploy-aws.yml +156 -32
  2. package/.github/workflows/deploy-cf-tcp.yml +11 -9
  3. package/.github/workflows/deploy-cf.yml +14 -14
  4. package/CHANGELOG.md +305 -0
  5. package/README.md +134 -41
  6. package/apps/aws-stack/README.md +3 -5
  7. package/apps/aws-stack/bin/aws.ts +82 -9
  8. package/apps/aws-stack/cdk.json +0 -3
  9. package/apps/aws-stack/package.json +3 -4
  10. package/apps/aws-stack/src/aws-stack.ts +177 -52
  11. package/apps/aws-stack/src/static-site.ts +323 -0
  12. package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
  13. package/apps/aws-stack/tests/stack.test.ts +267 -92
  14. package/apps/aws-stack/tests/static-site.test.ts +491 -0
  15. package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
  16. package/apps/cf-tcp-container/package.json +2 -3
  17. package/apps/cf-tcp-container/src/container-server.ts +12 -9
  18. package/apps/cf-tcp-container/wrangler.toml +1 -10
  19. package/apps/cf-worker/package.json +3 -4
  20. package/apps/cf-worker/wrangler.toml +12 -71
  21. package/apps/local-cli/package.json +1 -1
  22. package/apps/local-cli/src/server.ts +21 -17
  23. package/apps/web/landing/favicon.ico +0 -0
  24. package/apps/web/landing/index.html +1 -0
  25. package/apps/web/package.json +2 -2
  26. package/apps/web/scripts/build.mjs +66 -4
  27. package/apps/web/src/build-env.ts +125 -4
  28. package/apps/web/src/config-schema.ts +20 -6
  29. package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
  30. package/apps/web/tests/build-env.test.ts +210 -9
  31. package/apps/web/tests/build-smoke.test.ts +2 -2
  32. package/apps/web/tests/config-schema.test.ts +149 -25
  33. package/docs/AWS-Deployment.md +670 -96
  34. package/docs/AWS-TCP-Deployment.md +20 -45
  35. package/docs/Cloudflare-Deployment-Guide.md +87 -113
  36. package/docs/Cloudflare-TCP-Deployment.md +25 -49
  37. package/docs/Release-Process.md +27 -23
  38. package/docs/Services.md +69 -22
  39. package/docs/WebClientGuide.md +35 -26
  40. package/package.json +7 -10
  41. package/packages/aws-adapter/package.json +1 -1
  42. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
  43. package/packages/aws-adapter/src/config-loader.ts +19 -2
  44. package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
  45. package/packages/aws-adapter/src/handlers/connect.ts +26 -0
  46. package/packages/aws-adapter/src/handlers/default.ts +190 -123
  47. package/packages/aws-adapter/src/handlers/index.ts +67 -23
  48. package/packages/aws-adapter/src/handlers/nlb-stream.ts +3 -6
  49. package/packages/aws-adapter/src/index.ts +5 -7
  50. package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
  51. package/packages/aws-adapter/src/serialize.ts +15 -0
  52. package/packages/aws-adapter/src/tables.ts +2 -12
  53. package/packages/aws-adapter/tests/aws-harness.ts +0 -1
  54. package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
  55. package/packages/aws-adapter/tests/connect.test.ts +124 -1
  56. package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
  57. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -0
  58. package/packages/aws-adapter/tests/handlers.test.ts +117 -11
  59. package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
  60. package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
  61. package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
  62. package/packages/aws-adapter/tests/stats.test.ts +0 -3
  63. package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
  64. package/packages/aws-adapter/tests/tables.test.ts +1 -8
  65. package/packages/aws-adapter/tests/transactions.test.ts +0 -1
  66. package/packages/cf-adapter/package.json +1 -5
  67. package/packages/cf-adapter/src/cf-runtime.ts +59 -8
  68. package/packages/cf-adapter/src/channel-do.ts +13 -3
  69. package/packages/cf-adapter/src/connection-do.ts +266 -109
  70. package/packages/cf-adapter/src/d1-services-store.ts +63 -26
  71. package/packages/cf-adapter/src/env.ts +11 -10
  72. package/packages/cf-adapter/src/index.ts +0 -6
  73. package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
  74. package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
  75. package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
  76. package/packages/cf-adapter/tests/connection-do-pure.test.ts +95 -54
  77. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
  78. package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
  79. package/packages/cf-adapter/tests/serialize.test.ts +25 -0
  80. package/packages/in-memory-runtime/package.json +1 -1
  81. package/packages/irc-core/package.json +1 -1
  82. package/packages/irc-core/src/account-migration.ts +140 -0
  83. package/packages/irc-core/src/commands/account-auth.ts +16 -19
  84. package/packages/irc-core/src/commands/memoserv.ts +1 -1
  85. package/packages/irc-core/src/commands/nickserv.ts +122 -11
  86. package/packages/irc-core/src/commands/registration.ts +2 -2
  87. package/packages/irc-core/src/commands/sasl.ts +22 -31
  88. package/packages/irc-core/src/config.ts +35 -9
  89. package/packages/irc-core/src/credential-hashing.ts +11 -54
  90. package/packages/irc-core/src/index.ts +1 -0
  91. package/packages/irc-core/src/ports.ts +159 -179
  92. package/packages/irc-core/src/types.ts +38 -10
  93. package/packages/irc-core/tests/account-migration.test.ts +133 -0
  94. package/packages/irc-core/tests/commands/markread.test.ts +54 -0
  95. package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
  96. package/packages/irc-core/tests/commands/nickserv.test.ts +237 -1
  97. package/packages/irc-core/tests/commands/oper.test.ts +15 -0
  98. package/packages/irc-core/tests/commands/registration.test.ts +140 -133
  99. package/packages/irc-core/tests/commands/sasl.test.ts +159 -178
  100. package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
  101. package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
  102. package/packages/irc-core/tests/message-store.test.ts +5 -0
  103. package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
  104. package/packages/irc-core/tests/ports.test.ts +71 -0
  105. package/packages/irc-core/tests/services-store.test.ts +204 -0
  106. package/packages/irc-core/vitest.config.ts +6 -1
  107. package/packages/irc-server/package.json +1 -1
  108. package/packages/irc-server/src/actor.ts +0 -14
  109. package/packages/irc-server/tests/actor.test.ts +19 -47
  110. package/packages/irc-test-support/package.json +1 -1
  111. package/pnpm-workspace.yaml +1 -0
  112. package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
  113. package/scripts/deploy-web-aws.mjs +290 -0
  114. package/scripts/package.json +23 -0
  115. package/scripts/tsconfig.test.json +12 -0
  116. package/scripts/vitest.config.ts +19 -0
  117. package/tools/ci-hardening/package.json +1 -1
  118. package/tools/ci-hardening/src/index.ts +2 -0
  119. package/tools/ci-hardening/src/validate.ts +57 -0
  120. package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
  121. package/tools/ci-hardening/tests/validate.test.ts +42 -0
  122. package/tools/load-test/package.json +1 -1
  123. package/tools/migrate-accounts-to-services.ts +270 -0
  124. package/tools/package.json +2 -1
  125. package/tools/seed-aws-accounts.ts +35 -10
  126. package/tools/seed-cf-accounts.ts +42 -9
  127. package/tools/tcp-ws-forwarder/package.json +1 -1
  128. package/packages/aws-adapter/src/account-store.ts +0 -121
  129. package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
  130. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
  131. package/packages/aws-adapter/tests/account-store.test.ts +0 -276
  132. package/packages/cf-adapter/src/d1-account-store.ts +0 -198
  133. package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
  134. package/packages/irc-core/tests/account-store.test.ts +0 -131
@@ -0,0 +1,323 @@
1
+ /**
2
+ * `StaticSite` — the AWS web-client hosting substrate.
3
+ *
4
+ * Serves the shared `apps/web/dist` build artifact (the Kiwi IRC SPA +
5
+ * landing page) exclusively over CloudFront. The S3 origin bucket is
6
+ * private and grants the distribution via Origin Access Control (OAC),
7
+ * the modern AWS recommendation that supersedes the legacy Origin
8
+ * Access Identity (OAI). Direct S3 object access is denied; only the
9
+ * CloudFront URL serves the SPA.
10
+ *
11
+ * The bucket layout mirrors the CF Worker's `[assets]` binding (so the
12
+ * same `apps/web/dist` artifact ships on both platforms unchanged):
13
+ * - `index.html` → the project landing page (served at `/`)
14
+ * - `webclient/index.html` → the Kiwi SPA shell (served at `/webclient/`)
15
+ * - `webclient/static/*` → hashed JS/CSS bundles
16
+ * - `docs/*` → rendered documentation pages
17
+ *
18
+ * `https://<dist>/` serves `index.html` via the distribution's
19
+ * `defaultRootObject`. SPA client-side routing under `/webclient/*` is
20
+ * handled by a CloudFront custom error response that maps S3's 403/404
21
+ * (the response when an object does not exist) to
22
+ * `200 /webclient/index.html`, so a deep link like
23
+ * `/webclient/networks/example` resolves to the SPA shell and Kiwi's
24
+ * hash/history router takes over.
25
+ *
26
+ * This is pure infrastructure — it does NOT build or bake the artifact
27
+ * (that is the per-env config bake and the deploy pipeline). Custom
28
+ * domain (ACM cert + CloudFront alias + optional Route53 record) is
29
+ * exposed via optional construct props and used by prod; the default
30
+ * uses the `*.cloudfront.net` URL.
31
+ */
32
+
33
+ import { CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib';
34
+ import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
35
+ import {
36
+ Function as CloudFrontFunction,
37
+ Distribution,
38
+ FunctionCode,
39
+ FunctionEventType,
40
+ ViewerProtocolPolicy,
41
+ } from 'aws-cdk-lib/aws-cloudfront';
42
+ import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
43
+ import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
44
+ import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets';
45
+ import { BlockPublicAccess, Bucket } from 'aws-cdk-lib/aws-s3';
46
+ import { Construct } from 'constructs';
47
+
48
+ /**
49
+ * Path of the landing page inside the origin bucket. Served by the
50
+ * distribution's `defaultRootObject` for requests to `/` (the project
51
+ * front door — links to the SPA, the rendered docs, and the source
52
+ * repo). Mirrors `dist/index.html` from `apps/web/scripts/build.mjs`
53
+ * step 5.
54
+ */
55
+ const LANDING_INDEX_PATH = 'index.html';
56
+
57
+ /**
58
+ * Path to the SPA shell inside the origin bucket. CloudFront serves this
59
+ * for any unmatched `/webclient/*` route (via the 403/404 → 200 custom
60
+ * error response), so client-side routing resolves to the SPA. Mirrors
61
+ * `dist/webclient/index.html` from `apps/web/scripts/build.mjs` step 3
62
+ * — the same path the CF Worker's `[assets]` binding serves the SPA at.
63
+ */
64
+ const SPA_INDEX_PATH = '/webclient/index.html';
65
+
66
+ /**
67
+ * CloudFront viewer-request function that resolves directory indexes for
68
+ * the S3 REST origin. S3 REST (used with Origin Access Control) does NOT
69
+ * serve index documents: a request for `/docs/` or `/docs` looks up an
70
+ * object literally named `docs/` (or `docs`) and 404s, which then falls
71
+ * through to the SPA error response and serves the Kiwi shell instead of
72
+ * the rendered docs index (reproduced live on `irc.aws.fowl.cloud`).
73
+ *
74
+ * The function makes directory-style URIs resolve to their `index.html`,
75
+ * matching the Cloudflare Worker `[assets]` binding (which resolves
76
+ * directory indexes automatically). Two cases:
77
+ *
78
+ * - trailing slash (`/docs/`, `/`, `/webclient/`) → internal rewrite
79
+ * appending `index.html` (browser URL unchanged; relative links in
80
+ * the page already resolve against the directory);
81
+ * - no trailing slash AND the last path segment has no file extension
82
+ * (`/docs`, `/webclient`) → **301 redirect** to the trailing-slash
83
+ * form. A redirect is used (rather than a silent rewrite to
84
+ * `<path>/index.html`) so the BROWSER URL becomes `<path>/` and the
85
+ * page's relative asset links (`<link href="docs.css">`) resolve
86
+ * against the directory (`/docs/docs.css`) instead of the parent
87
+ * (`/docs.css`). This mirrors Apache/nginx `DirectorySlash`.
88
+ *
89
+ * URIs whose last segment contains a `.` (every real static asset:
90
+ * `.html`, `.css`, `.js`, `.png`, `.json`, `.woff2`, `.ico`) are served
91
+ * unchanged. The redirect is safe: S3 REST currently resolves no
92
+ * no-extension path to a real object, so redirecting only ever helps
93
+ * (the trailing-slash form then resolves via the internal rewrite) or
94
+ * stays equivalent (SPA deep links like `/webclient/<route>` redirect
95
+ * to `<route>/`, then 404 → the SPA error response still serves
96
+ * `/webclient/index.html`, and Kiwi's router resolves from the URL).
97
+ * AWS publishes this pattern for S3-backed CloudFront distributions.
98
+ *
99
+ * Implemented with `var` + `indexOf`/`lastIndexOf`/`slice`/`endsWith`
100
+ * only — the CloudFront Functions runtime supports these (ES5.1 + the
101
+ * documented `String.prototype.endsWith`).
102
+ */
103
+ const INDEX_DOCUMENT_FN_CODE = [
104
+ 'function handler(event) {',
105
+ ' var request = event.request;',
106
+ ' var uri = request.uri;',
107
+ ' // Trailing slash: resolve to the directory index.html (internal rewrite).',
108
+ ' if (uri.endsWith("/")) {',
109
+ ' request.uri = uri + "index.html";',
110
+ ' return request;',
111
+ ' }',
112
+ ' // File (last path segment has an extension): serve unchanged.',
113
+ ' var lastSegment = uri.slice(uri.lastIndexOf("/") + 1);',
114
+ ' if (lastSegment.indexOf(".") !== -1) {',
115
+ ' return request;',
116
+ ' }',
117
+ ' // Directory path without a trailing slash: 301-redirect to the',
118
+ ' // trailing-slash form so the browser resolves relative asset URLs',
119
+ ' // against the directory (href="docs.css" -> /docs/docs.css, not',
120
+ ' // /docs.css). Rebuild the query string from the CloudFront Functions',
121
+ ' // object form ({ key: { value: "..." } }); request.querystring is',
122
+ ' // ALWAYS an object ({} when empty), never a raw string, so a truthy',
123
+ ' // check alone would append "?[object Object]".',
124
+ ' var qsRoot = request.querystring;',
125
+ ' var qsKeys = qsRoot ? Object.keys(qsRoot) : [];',
126
+ ' var qs = "";',
127
+ ' for (var qi = 0; qi < qsKeys.length; qi++) {',
128
+ ' var qk = qsKeys[qi];',
129
+ ' qs += (qs.length === 0 ? "?" : "&") + encodeURIComponent(qk) + "=" + encodeURIComponent(request.querystring[qk].value);',
130
+ ' }',
131
+ ' return {',
132
+ ' statusCode: 301,',
133
+ ' statusDescription: "Moved Permanently",',
134
+ ' headers: { location: { value: uri + "/" + qs } }',
135
+ ' };',
136
+ '}',
137
+ ].join('\n');
138
+
139
+ /**
140
+ * Construct props for {@link StaticSite}.
141
+ *
142
+ * Custom domain is opt-in: when `customDomain` is omitted the
143
+ * distribution is reachable only via its default `*.cloudfront.net`
144
+ * domain (suitable for staging). Prod passes all three fields to wire a
145
+ * CloudFront alias + Route53 `ARecord`.
146
+ */
147
+ export interface StaticSiteProps {
148
+ /**
149
+ * Custom domain name for the CloudFront distribution (e.g.
150
+ * `app.example.com`). When provided together with {@link certificateArn}
151
+ * the distribution is configured with this alias.
152
+ *
153
+ * @default - the distribution uses its default `*.cloudfront.net` domain.
154
+ */
155
+ readonly customDomain?: string;
156
+
157
+ /**
158
+ * ARN of an ACM certificate covering {@link customDomain}. The
159
+ * certificate MUST be in `us-east-1` (a CloudFront requirement). Only
160
+ * consulted when {@link customDomain} is set.
161
+ */
162
+ readonly certificateArn?: string;
163
+
164
+ /**
165
+ * Name of the Route53 hosted zone to add the alias record into (e.g.
166
+ * `example.com.`). When provided together with {@link customDomain} and
167
+ * {@link hostedZoneId} an `ARecord` aliasing the distribution is created.
168
+ * Only consulted when {@link customDomain} is set.
169
+ */
170
+ readonly hostedZoneName?: string;
171
+
172
+ /**
173
+ * Public Route53 hosted zone ID for {@link hostedZoneName}. Required to
174
+ * materialise the alias record (CDK's `fromHostedZoneAttributes` needs
175
+ * both the name and the ID; `fromLookup` is avoided so `cdk synth` does
176
+ * not require AWS credentials). Discoverable via
177
+ * `aws route53 list-hosted-zones-by-name --dns-name <hostedZoneName>`.
178
+ * Only consulted when {@link customDomain} is set.
179
+ */
180
+ readonly hostedZoneId?: string;
181
+ }
182
+
183
+ /**
184
+ * Serves the web client over CloudFront backed by a private S3 bucket
185
+ * with Origin Access Control.
186
+ *
187
+ * The bucket is never public-read; CloudFront reads it via an OAC and a
188
+ * bucket policy scoped to this distribution (`AWS:SourceArn`). The
189
+ * landing page at `/` is served via the distribution's
190
+ * `defaultRootObject` (`index.html`); a custom error response maps
191
+ * 403/404 → `200 /webclient/index.html` so SPA routes under
192
+ * `/webclient/*` resolve. The bucket layout mirrors the CF Worker's
193
+ * `[assets]` binding so the same `apps/web/dist` artifact ships on
194
+ * both platforms unchanged.
195
+ */
196
+ export class StaticSite extends Construct {
197
+ /** The private S3 origin bucket holding the web client artifact. */
198
+ readonly bucket: Bucket;
199
+
200
+ /** The CloudFront distribution serving the SPA. */
201
+ readonly distribution: Distribution;
202
+
203
+ constructor(scope: Construct, id: string, props: StaticSiteProps = {}) {
204
+ super(scope, id);
205
+
206
+ // Private origin bucket. Block ALL public access and attach no public
207
+ // read ACL — the bucket is reachable only through CloudFront via the
208
+ // OAC-granted bucket policy. AutoDeleteObjects + DESTROY keeps the
209
+ // dev/test stack clean on `cdk destroy` (the deploy pipeline owns
210
+ // the object lifecycle in real envs via s3 sync).
211
+ this.bucket = new Bucket(this, 'Bucket', {
212
+ blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
213
+ removalPolicy: RemovalPolicy.DESTROY,
214
+ autoDeleteObjects: true,
215
+ });
216
+
217
+ // CloudFront distribution. The default behavior routes to the S3
218
+ // origin via OAC (`S3BucketOrigin.withOriginAccessControl` auto-creates
219
+ // the `S3OriginAccessControl` and the `cloudfront.amazonaws.com`
220
+ // service-principal bucket policy scoped by `AWS:SourceArn`).
221
+ // HTTPS is enforced by redirecting HTTP viewers.
222
+ //
223
+ // The `as never` cast on the bucket bypasses an `exactOptionalPropertyTypes`
224
+ // friction between the concrete `Bucket` and the `IBucket` interface
225
+ // (optional vs required properties in the CDK jsii layer); the same
226
+ // pattern is used elsewhere in this stack for the NLB `Vpc`.
227
+ const origin = S3BucketOrigin.withOriginAccessControl(this.bucket as never);
228
+
229
+ // Directory-index resolver. S3 REST (OAC) does not serve index
230
+ // documents, so `/docs/` and `/docs` would 404 and fall through to the
231
+ // SPA error response below. This viewer-request function appends
232
+ // `index.html` to directory-style URIs first, mirroring the CF Worker
233
+ // `[assets]` binding (see {@link INDEX_DOCUMENT_FN_CODE} for the full
234
+ // rationale + the SPA-deep-link interaction).
235
+ const indexFunction = new CloudFrontFunction(this, 'IndexDocumentFunction', {
236
+ code: FunctionCode.fromInline(INDEX_DOCUMENT_FN_CODE),
237
+ });
238
+
239
+ // SPA fallback: S3 returns 403/404 for any `/webclient/*` path that is
240
+ // not a real object (i.e. a client-side route). Map both to a `200`
241
+ // response serving the SPA shell so Kiwi's router can resolve the
242
+ // deep link. The landing page at `/` is unaffected (served via
243
+ // `defaultRootObject` below, not via the error response).
244
+ const errorResponses = [
245
+ {
246
+ httpStatus: 403,
247
+ responseHttpStatus: 200,
248
+ responsePagePath: SPA_INDEX_PATH,
249
+ ttl: Duration.seconds(0),
250
+ },
251
+ {
252
+ httpStatus: 404,
253
+ responseHttpStatus: 200,
254
+ responsePagePath: SPA_INDEX_PATH,
255
+ ttl: Duration.seconds(0),
256
+ },
257
+ ];
258
+
259
+ // Optional custom domain. Only wired when `customDomain` is provided;
260
+ // otherwise the distribution uses its default `*.cloudfront.net`
261
+ // domain (staging). The certificate MUST be in us-east-1 (a CloudFront
262
+ // requirement) — validated at deploy time.
263
+ const domainNames = props.customDomain !== undefined ? [props.customDomain] : undefined;
264
+ const certificate =
265
+ props.customDomain !== undefined && props.certificateArn !== undefined
266
+ ? Certificate.fromCertificateArn(this, 'Certificate', props.certificateArn)
267
+ : undefined;
268
+
269
+ this.distribution = new Distribution(this, 'Distribution', {
270
+ // The landing page (`dist/index.html`, the project front door with
271
+ // links to the SPA + docs + source repo) is served at `/`. The SPA
272
+ // shell lives at `/webclient/index.html` and is reached either
273
+ // directly or via the 403/404 error-response fallback for deep
274
+ // links. CloudFront's `DefaultRootObject` does NOT take a leading
275
+ // slash (unlike `responsePagePath`).
276
+ defaultRootObject: LANDING_INDEX_PATH,
277
+ defaultBehavior: {
278
+ origin,
279
+ viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
280
+ functionAssociations: [
281
+ { eventType: FunctionEventType.VIEWER_REQUEST, function: indexFunction },
282
+ ],
283
+ },
284
+ errorResponses,
285
+ ...(domainNames !== undefined ? { domainNames } : {}),
286
+ ...(certificate !== undefined ? { certificate } : {}),
287
+ });
288
+
289
+ // Route53 alias record for the custom domain. Only created when the
290
+ // custom domain AND its hosted zone (name + id) are supplied; the
291
+ // certificate is optional only in the sense that CloudFront requires
292
+ // it for an alias, so a missing cert surfaces as a deploy-time CFN
293
+ // error rather than a synth-time crash. The zone is imported via
294
+ // `fromHostedZoneAttributes` (explicit name + id) rather than
295
+ // `fromLookup` so `cdk synth` does not require AWS credentials.
296
+ if (
297
+ props.customDomain !== undefined &&
298
+ props.hostedZoneName !== undefined &&
299
+ props.hostedZoneId !== undefined
300
+ ) {
301
+ const zone = HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
302
+ zoneName: props.hostedZoneName,
303
+ hostedZoneId: props.hostedZoneId,
304
+ });
305
+ new ARecord(this, 'AliasRecord', {
306
+ zone,
307
+ recordName: props.customDomain,
308
+ target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
309
+ });
310
+ }
311
+
312
+ // Pin the logical id to the stable key `WebsiteURL` so the deployed
313
+ // stack exposes `Stack.WebsiteURL` rather than the CDK-hashed default
314
+ // (`StaticSiteWebsiteURL<8-hex>`). Matches the convention used by the
315
+ // parent-stack outputs (`WebsiteBucketName`, `WebsiteDistributionId`)
316
+ // and keeps `describe-stacks --query` lookups predictable.
317
+ const urlOutput = new CfnOutput(this, 'WebsiteURL', {
318
+ description: 'CloudFront distribution domain serving the web client (HTTPS).',
319
+ value: this.distribution.domainName,
320
+ });
321
+ urlOutput.overrideLogicalId('WebsiteURL');
322
+ }
323
+ }
@@ -61,7 +61,7 @@ describe('parseArgs', () => {
61
61
  });
62
62
 
63
63
  it('tolerates a leading -- options terminator (pnpm script passthrough)', () => {
64
- // `pnpm smoke:aws:staging -- --url wss://x` forwards `['--', '--url', 'wss://x']`.
64
+ // `pnpm smoke:aws -- --url wss://x` forwards `['--', '--url', 'wss://x']`.
65
65
  expect(parseArgs(['--', '--url', 'wss://irc.example.com/prod'])).toStrictEqual<ParseResult>({
66
66
  url: 'wss://irc.example.com/prod',
67
67
  help: false,