serverless-ircd 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +11 -9
- package/.github/workflows/deploy-cf.yml +14 -14
- package/CHANGELOG.md +305 -0
- package/README.md +134 -41
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +82 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +177 -52
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +267 -92
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/package.json +2 -3
- package/apps/cf-tcp-container/src/container-server.ts +12 -9
- package/apps/cf-tcp-container/wrangler.toml +1 -10
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/wrangler.toml +12 -71
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +21 -17
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/docs/AWS-Deployment.md +670 -96
- package/docs/AWS-TCP-Deployment.md +20 -45
- package/docs/Cloudflare-Deployment-Guide.md +87 -113
- package/docs/Cloudflare-TCP-Deployment.md +25 -49
- package/docs/Release-Process.md +27 -23
- package/docs/Services.md +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
- package/packages/aws-adapter/src/config-loader.ts +19 -2
- package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
- package/packages/aws-adapter/src/handlers/connect.ts +26 -0
- package/packages/aws-adapter/src/handlers/default.ts +190 -123
- package/packages/aws-adapter/src/handlers/index.ts +67 -23
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +3 -6
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +15 -0
- package/packages/aws-adapter/src/tables.ts +2 -12
- package/packages/aws-adapter/tests/aws-harness.ts +0 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
- package/packages/aws-adapter/tests/connect.test.ts +124 -1
- package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -0
- package/packages/aws-adapter/tests/handlers.test.ts +117 -11
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +59 -8
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/connection-do.ts +266 -109
- package/packages/cf-adapter/src/d1-services-store.ts +63 -26
- package/packages/cf-adapter/src/env.ts +11 -10
- package/packages/cf-adapter/src/index.ts +0 -6
- package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +95 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +122 -11
- package/packages/irc-core/src/commands/registration.ts +2 -2
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- package/packages/irc-core/src/config.ts +35 -9
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +159 -179
- package/packages/irc-core/src/types.ts +38 -10
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +237 -1
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +140 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +159 -178
- package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
- package/packages/irc-core/tests/ports.test.ts +71 -0
- package/packages/irc-core/tests/services-store.test.ts +204 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +0 -14
- package/packages/irc-server/tests/actor.test.ts +19 -47
- package/packages/irc-test-support/package.json +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/src/index.ts +2 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/load-test/package.json +1 -1
- package/tools/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -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
|
|
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,
|