canopycms-cdk 0.0.67 → 0.0.68-int.100

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/README.md CHANGED
@@ -14,6 +14,7 @@ AWS CDK constructs and EC2 worker for deploying CanopyCMS.
14
14
  - Lambda Function URL (for CloudFront origin)
15
15
  - EC2 Worker (t4g.nano spot in ASG, public subnet, EFS mount)
16
16
  - Security groups and IAM roles (least-privilege)
17
+ - `attachTo(distribution)`: adds the editor's `/edit`, `/edit/*` and `/api/canopycms/*` behaviors (and, optionally, the CMS build's `assetPrefix` and the preview route) to a CloudFront distribution you already own
17
18
 
18
19
  **`CanopyCmsDistribution`** — CloudFront + DNS (optional):
19
20
 
@@ -21,8 +22,9 @@ AWS CDK constructs and EC2 worker for deploying CanopyCMS.
21
22
  - CloudFront distribution with Lambda Function URL origin
22
23
  - Route53 A/AAAA alias records
23
24
  - Cache policies: no-cache for API/editor, long-cache for static assets
25
+ - Response headers: framing protection (`frame-ancestors 'self'`), `nosniff`, HSTS, `X-Robots-Tag: noindex`
24
26
 
25
- Use `CanopyCmsDistribution` if you don't have existing CloudFront infrastructure. Otherwise, use the `functionUrl` output from `CanopyCmsService` and wire it into your own CloudFront setup.
27
+ Use `CanopyCmsDistribution` when the CMS gets its own domain. To serve the editor from an existing distribution, call `cmsService.attachTo(distribution)` instead of wiring the `functionUrl` by hand; see [Serving the editor from a distribution you already own](../../docs/deploying-to-aws.md#serving-the-editor-from-a-distribution-you-already-own).
26
28
 
27
29
  ### EC2 Worker
28
30
 
@@ -128,50 +128,68 @@ export interface AssetSupportProps {
128
128
  */
129
129
  readonly autoDeleteObjects?: boolean;
130
130
  /**
131
- * Require the transform Lambda's code asset to carry proof that it was
132
- * built with its native sharp binary (the `.deployable` marker written by
133
- * `build:lambda`). Leave this ON for anything that can reach a real
134
- * deploy.
131
+ * A replica of the bucket's `assets/` prefix. Both public behaviors fail over
132
+ * to it on 500, 502, 503 or 504 from the primary, never on a miss: the
133
+ * replica only holds what the primary has. With `lazyPublicTransforms`,
134
+ * `/assets/t/*`'s one fallback slot is the transform Lambda, so only
135
+ * `/assets/*` gets the replica.
135
136
  *
136
- * Set to `false` ONLY in this package's own tests, which synth against the
137
- * cheap `--skip-native` fixture bundle `build:test-fixtures` produces: the
138
- * suite never executes the handler, so the binary is irrelevant to what it
139
- * asserts, and requiring a real build would put a live `npm install sharp` in
140
- * front of every test run and price the suite out of CI.
137
+ * It is read through its own OAC, whose grant CDK writes only on a bucket it
138
+ * owns: an imported replica's policy must allow this distribution itself.
139
+ * Import one in another region with `Bucket.fromBucketAttributes({ bucketName,
140
+ * region })` so its regional domain name is right. Replication must cover
141
+ * `assets/`; `materialize-assets` writes only the primary.
141
142
  *
142
- * Adopters never need this: the published package's asset is built by
143
- * `prepack`'s full `build:lambda`, so the marker is always present.
143
+ * @default - no failover
144
+ */
145
+ readonly replicaBucket?: s3.IBucket;
146
+ /**
147
+ * Compute a missing `/assets/t/*` derivative on request with a transform
148
+ * Lambda, instead of serving only what `canopycms materialize-assets` wrote.
149
+ * Off, the public path computes nothing and an unmaterialized URL is a miss,
150
+ * which is also what a live preview on another origin than the editor shows
151
+ * for a draft's new crop.
144
152
  *
145
- * @default true
153
+ * On, anyone can mint any allowlisted transform of a public asset - bounded
154
+ * per asset except crop, at ~10^16 rects - capped by reserved concurrency and
155
+ * an `assets/t/` expiry that applies only to objects the Lambda writes, which
156
+ * carry the tag `canopy-transform=lazy`: what `materialize-assets` writes is
157
+ * never expired. On a BYO `bucket` this construct cannot write that rule, so
158
+ * lazy mode there requires an explicit `transformOutputRetention`.
159
+ *
160
+ * @default false
146
161
  */
147
- readonly requireDeployableBundle?: boolean;
162
+ readonly lazyPublicTransforms?: boolean;
148
163
  /**
149
- * Retention for the transform Lambda's CloudWatch log group (default:
150
- * three months / 90 days).
164
+ * Require the transform Lambda's code asset to carry the `.deployable`
165
+ * marker a full `build:lambda` writes, proving it has its native sharp
166
+ * binary. Checked only with `lazyPublicTransforms`. Set `false` ONLY in this
167
+ * package's own tests, which synth against the `--skip-native` fixture.
168
+ *
169
+ * @default true
151
170
  */
171
+ readonly requireDeployableBundle?: boolean;
172
+ /** Lazy mode only. Retention for the transform Lambda's log group (default 90 days). */
152
173
  readonly transformLogRetention?: logs.RetentionDays;
153
174
  /**
154
- * Concurrency cap on the transform Lambda (default: 10).
155
- *
156
- * This is a reservation — a CAP carved from the account's concurrency pool,
157
- * not pre-warmed capacity, so it costs nothing when idle. It bounds the
158
- * blast radius of the anonymous `/assets/t/*` path; see the default
159
- * constant's doc comment. Raise it for an unusually image-heavy site;
160
- * setting it to 0 would disable transforms entirely.
175
+ * Lazy mode only. Concurrency cap on the transform Lambda (default 10), a
176
+ * reservation that costs nothing idle. 0 disables transforms.
161
177
  */
162
178
  readonly transformReservedConcurrency?: number;
163
179
  /**
164
- * How long generated derivatives under `assets/t/` are kept (default: 180
165
- * days). Only applies to a bucket this construct creates — in BYO-bucket
166
- * mode the caller owns lifecycle rules.
167
- *
168
- * These objects are regenerable; expiry is self-healing (the next request
169
- * regenerates and re-stores). Source assets under `asset-originals/` are
170
- * never affected.
180
+ * Lazy mode only. How long the Lambda's `assets/t/` outputs are kept
181
+ * (default 180 days), written as a lifecycle rule on a bucket this construct
182
+ * creates. On a BYO `bucket` nothing is written and the prop is required:
183
+ * passing it states that your bucket expires them itself, filtering on the
184
+ * tag `canopy-transform=lazy` so what `materialize-assets` writes survives,
185
+ * and on a versioned bucket also expiring noncurrent versions. Remove that
186
+ * rule before leaving lazy mode: a key the Lambda wrote first stays tagged
187
+ * after a build references it. A cross-account bucket's policy must also
188
+ * grant the transform role `s3:PutObjectTagging` on `assets/t/*`.
171
189
  */
172
190
  readonly transformOutputRetention?: Duration;
173
191
  /**
174
- * Execution role for the transform Lambda (default: CDK creates one).
192
+ * Lazy mode only. Execution role for the transform Lambda (default: CDK creates one).
175
193
  *
176
194
  * Set this when the role's ARN has to be computable WITHOUT a reference to
177
195
  * this construct - the motivating case is an asset bucket in a different AWS
@@ -192,11 +210,10 @@ export interface AssetSupportProps {
192
210
  */
193
211
  readonly transformRole?: iam.Role;
194
212
  /**
195
- * Name for the transform Lambda's CloudWatch log group (default:
213
+ * Lazy mode only. Name for the transform Lambda's CloudWatch log group (default:
196
214
  * `/canopycms/<stackName>/transform`). Deliberately NOT
197
- * `/aws/lambda/<function-name>` - see `transformLogGroup`'s comment in the
198
- * constructor for why a CDK-managed group must avoid that exact name once
199
- * the function has ever been deployed without one. Override to follow an
215
+ * `/aws/lambda/<function-name>` - see the log group's comment in
216
+ * `buildTransformLambda` for why. Override to follow an
200
217
  * org naming convention, or when instantiating this construct twice in one
201
218
  * stack (the default name would collide).
202
219
  */
@@ -249,10 +266,8 @@ export declare const ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS: readonly ["assets", "as
249
266
  * ```ts
250
267
  * const behaviors = assetSupport.assetBehaviors()
251
268
  *
252
- * // CloudFront matches path patterns in the order listed and stops at the
253
- * // first match, so the more specific '/assets/t/*' MUST come before
254
- * // '/assets/*' - otherwise the broader S3-only pattern swallows transform
255
- * // requests and they 403 with no Lambda fallback.
269
+ * // CloudFront stops at the first matching pattern, so '/assets/t/*' MUST
270
+ * // come before '/assets/*', or lazy mode's misses never reach the Lambda.
256
271
  * new cloudfront.Distribution(this, 'Dist', {
257
272
  * defaultBehavior: ...,
258
273
  * additionalBehaviors: {
@@ -264,18 +279,13 @@ export declare const ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS: readonly ["assets", "as
264
279
  */
265
280
  export interface AssetCloudFrontBehaviors {
266
281
  /**
267
- * `/assets/*` - static objects only (sanitized SVG/PDF the finalize
268
- * pipeline wrote, plus already-computed transform outputs under
269
- * `assets/t/...`, which also live under this prefix). S3 origin only -
270
- * nothing here is ever computed on demand.
282
+ * `/assets/*`: the S3 read origin (an origin group with `replicaBucket`).
271
283
  */
272
284
  readonly assets: cloudfront.BehaviorOptions;
273
285
  /**
274
- * `/assets/t/*` - transform outputs specifically. Origin group: the same
275
- * S3 origin as `assets` primary, falling over to the transform Lambda's
276
- * Function URL on 403 OR 404 (a signed OAC origin reports a miss as 403;
277
- * configuring both is defense-in-depth). CloudFront caches the failover
278
- * response.
286
+ * `/assets/t/*`: the same read origin as `assets`, or with
287
+ * `lazyPublicTransforms` an origin group falling over to the transform
288
+ * Lambda on 403 or 404.
279
289
  */
280
290
  readonly assetsTransform: cloudfront.BehaviorOptions;
281
291
  }
@@ -312,15 +322,8 @@ export interface AssetUploadBehaviorRouteOptions extends AssetUploadBehaviorOpti
312
322
  * ```
313
323
  *
314
324
  * Separate from `AssetSupport.uploadBehavior()` because the upload route needs
315
- * the bucket and nothing else, while that construct's CONSTRUCTOR always builds
316
- * the transform Lambda, a log group, a Function URL, a role and prefix grants.
317
- * Those grants never reach the BUCKET policy: `Grant.addToPrincipalOrResource`
318
- * stops after the identity statement for a same-account grantee, and
319
- * `addToResourcePolicy` on a bucket CDK does not own is a no-op - an owned
320
- * bucket, one imported by name, and one imported by ARN in another account all
321
- * emit zero `AWS::S3::BucketPolicy`, so a cross-account adopter writes the
322
- * resource half on the bucket's own side. Prefer the method when you already
323
- * have an `AssetSupport`; both funnel into the same builder and cannot drift.
325
+ * the bucket and nothing else. Prefer the method when you already have an
326
+ * `AssetSupport`; both funnel into the same builder and cannot drift.
324
327
  *
325
328
  * No bucket CORS rule is written and none is needed: the edge supplies
326
329
  * `Access-Control-Allow-Origin`, and S3's ACCEPTANCE of a presigned POST is
@@ -336,12 +339,12 @@ export declare function assetUploadBehavior(scope: Construct, options: AssetUplo
336
339
  * Wires:
337
340
  *
338
341
  * - The bucket's asset-prefix lifecycle rule + CORS (standalone mode only).
339
- * - The transform Lambda (`../../lambda/asset-transform/handler.ts`, built
340
- * via `pnpm run build:lambda` - see that script's doc comment for the
341
- * no-Docker sharp bundling approach), its dedicated CloudWatch log group
342
- * (custom name/retention/removal policy instead of the
343
- * CloudFormation-implicit `/aws/lambda/<function-name>` group), and its
344
- * OAC-locked Function URL.
342
+ * - The public read path: `/assets/*` and `/assets/t/*` served from S3 (plus
343
+ * `replicaBucket` on 5xx). It computes nothing; derivatives come from
344
+ * `canopycms materialize-assets`.
345
+ * - With `lazyPublicTransforms`, the transform Lambda
346
+ * (`../../lambda/asset-transform/handler.ts`, built by `build:lambda`), its
347
+ * log group and its OAC-locked Function URL, behind `/assets/t/*`.
345
348
  * - `attachTo(distribution)`, which attaches the two CloudFront behaviors a
346
349
  * consuming distribution needs in the only safe order (see its doc comment
347
350
  * for why the order is the whole point); `assetBehaviors()` is the escape
@@ -356,11 +359,12 @@ export declare function assetUploadBehavior(scope: Construct, options: AssetUplo
356
359
  export declare class AssetSupport extends Construct {
357
360
  /** The bucket in use (either created here, or the BYO `props.bucket`). */
358
361
  readonly bucket: s3.IBucket;
359
- readonly transformFunction: lambda.Function;
360
- /** The transform Lambda's CloudWatch log group (Lambda stdout/stderr). */
361
- readonly transformLogGroup: logs.LogGroup;
362
- /** The transform Lambda's Function URL - use as a CloudFront origin (see `assetBehaviors()`). */
363
- readonly transformFunctionUrl: lambda.FunctionUrl;
362
+ /** The lazy transform Lambda; undefined unless `lazyPublicTransforms`. */
363
+ readonly transformFunction: lambda.Function | undefined;
364
+ /** The transform Lambda's CloudWatch log group; undefined unless `lazyPublicTransforms`. */
365
+ readonly transformLogGroup: logs.LogGroup | undefined;
366
+ /** The transform Lambda's Function URL; undefined unless `lazyPublicTransforms`. */
367
+ readonly transformFunctionUrl: lambda.FunctionUrl | undefined;
364
368
  /** Effective advisory upload-size cap (see `AssetSupportProps.maxUploadBytes`). */
365
369
  readonly maxUploadBytes: number;
366
370
  private readonly behaviors;
@@ -388,6 +392,7 @@ export declare class AssetSupport extends Construct {
388
392
  */
389
393
  private uploadOrigin?;
390
394
  constructor(scope: Construct, id: string, props: AssetSupportProps);
395
+ private buildTransformLambda;
391
396
  private buildBehaviors;
392
397
  /**
393
398
  * The two CloudFront behavior configs this system needs.
@@ -424,9 +429,8 @@ export declare class AssetSupport extends Construct {
424
429
  * site's cookies and any cached basic-auth credential are live hazards
425
430
  * there, stripped by policy and function rather than never sent at all.
426
431
  * - NOT one shared distribution serving asset reads AND writes for every
427
- * environment. That means one `AssetSupport`, which owns the transform
428
- * Lambda shipping inside this package, so a `canopycms-cdk` bump would move
429
- * every environment's asset pipeline at once. Only the upload route moves;
432
+ * environment. That means one `AssetSupport`, so a `canopycms-cdk` bump
433
+ * would move every environment's asset pipeline at once. Only the upload route moves;
430
434
  * it depends on the bucket and nothing else, the same property that lets
431
435
  * `assetUploadBehavior` build it from a bucket alone.
432
436
  */
@@ -435,17 +439,12 @@ export declare class AssetSupport extends Construct {
435
439
  * Attach both asset behaviors to a concrete CloudFront distribution, in the
436
440
  * only safe order.
437
441
  *
438
- * THE ORDER IS THE WHOLE POINT. CloudFront matches path patterns in the order
439
- * given and stops at the first match. `/assets/*` is a broader, S3-only
440
- * pattern that also matches every `/assets/t/*` request; `/assets/t/*` is an
441
- * origin group that fails over to the transform Lambda on a miss. Attach
442
- * `/assets/*` first and every never-yet-computed transform gets a permanent
443
- * 403 (an OAC-signed S3 miss reports 403) while already-computed transforms
444
- * keep working. `'/assets/*'` also sorts BEFORE `'/assets/t/*'`
445
- * lexicographically (`*` = 0x2A, `t` = 0x74), so alphabetizing the keys
446
- * reproduces exactly this failure with no synth or deploy error to catch it -
447
- * and `assetBehaviors()` attaches no path pattern, so nothing there stops a
448
- * caller getting it wrong.
442
+ * CloudFront stops at the first matching pattern, and `/assets/*` also
443
+ * matches every `/assets/t/*` request. With `lazyPublicTransforms`, attaching
444
+ * it first means no miss ever reaches the transform Lambda, with no synth or
445
+ * deploy error; `'/assets/*'` also sorts first alphabetically. The two
446
+ * behaviors are identical otherwise, and the order is kept so that switching
447
+ * modes changes nothing else.
449
448
  *
450
449
  * `overrides` is merged into BOTH behaviors. The motivating case is a
451
450
  * distribution running a viewer-request function on every behavior - tier