canopycms-cdk 0.0.67 → 0.0.68-int.101

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,91 @@ 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
+ * Deny every `s3:PutObject` to `assets/*`, `asset-originals/*` and `asset-meta/*` that carries
132
+ * no `If-None-Match`, so no principal can replace an object under those content-addressed
133
+ * prefixes. Standalone mode only: with a BYO `bucket` the prop is refused, and the same statement
134
+ * belongs in that bucket's own policy (see docs/deploying-to-aws.md).
135
135
  *
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.
136
+ * Opt-in because the Deny binds every writer: the CMS Lambda, which runs the `canopycms` your app
137
+ * installs, and with `lazyPublicTransforms` the transform Lambda, which bundles the `canopycms`
138
+ * this package was built with and calls S3 through the Lambda runtime's AWS SDK. Upgrade and
139
+ * deploy both to releases whose stores write create-only, then enable this. It also denies every
140
+ * unconditional write, such as a plain `aws s3 cp`, and every multipart upload (`UploadPart`
141
+ * carries no `If-None-Match`). Replication is authorized as `s3:ReplicateObject`, so it is not
142
+ * denied; a replica bucket takes the same statement.
141
143
  *
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.
144
+ * Create-only holds only as far as `s3:DeleteObject` does: a delete then a create replaces an
145
+ * object, so keep Delete on `assets/*` and `asset-originals/*` to a break-glass role. That is
146
+ * also how a bad derivative is replaced: delete it, then rerun `materialize-assets`. On a
147
+ * versioned bucket the delete writes a delete marker and the bad version stays restorable; a
148
+ * replica keeps the bad version until the rematerialized object replicates.
144
149
  *
145
- * @default true
150
+ * @default false
146
151
  */
147
- readonly requireDeployableBundle?: boolean;
152
+ readonly enforceCreateOnlyWrites?: boolean;
148
153
  /**
149
- * Retention for the transform Lambda's CloudWatch log group (default:
150
- * three months / 90 days).
154
+ * A replica of the bucket's `assets/` prefix. Both public behaviors fail over
155
+ * to it on 500, 502, 503 or 504 from the primary, never on a miss: the
156
+ * replica only holds what the primary has. With `lazyPublicTransforms`,
157
+ * `/assets/t/*`'s one fallback slot is the transform Lambda, so only
158
+ * `/assets/*` gets the replica.
159
+ *
160
+ * It is read through its own OAC, whose grant CDK writes only on a bucket it
161
+ * owns: an imported replica's policy must allow this distribution itself.
162
+ * Import one in another region with `Bucket.fromBucketAttributes({ bucketName,
163
+ * region })` so its regional domain name is right. Replication must cover
164
+ * `assets/`; `materialize-assets` writes only the primary.
165
+ *
166
+ * @default - no failover
151
167
  */
152
- readonly transformLogRetention?: logs.RetentionDays;
168
+ readonly replicaBucket?: s3.IBucket;
153
169
  /**
154
- * Concurrency cap on the transform Lambda (default: 10).
170
+ * Compute a missing `/assets/t/*` derivative on request with a transform
171
+ * Lambda, instead of serving only what `canopycms materialize-assets` wrote.
172
+ * Off, the public path computes nothing and an unmaterialized URL is a miss,
173
+ * which is also what a live preview on another origin than the editor shows
174
+ * for a draft's new crop.
175
+ *
176
+ * On, anyone can mint any allowlisted transform of a public asset - bounded
177
+ * per asset except crop, at ~10^16 rects - capped by reserved concurrency and
178
+ * an `assets/t/` expiry that applies only to objects the Lambda writes, which
179
+ * carry the tag `canopy-transform=lazy`: what `materialize-assets` writes is
180
+ * never expired. On a BYO `bucket` this construct cannot write that rule, so
181
+ * lazy mode there requires an explicit `transformOutputRetention`.
155
182
  *
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.
183
+ * @default false
161
184
  */
162
- readonly transformReservedConcurrency?: number;
185
+ readonly lazyPublicTransforms?: boolean;
163
186
  /**
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.
187
+ * Require the transform Lambda's code asset to carry the `.deployable`
188
+ * marker a full `build:lambda` writes, proving it has its native sharp
189
+ * binary. Checked only with `lazyPublicTransforms`. Set `false` ONLY in this
190
+ * package's own tests, which synth against the `--skip-native` fixture.
167
191
  *
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.
192
+ * @default true
193
+ */
194
+ readonly requireDeployableBundle?: boolean;
195
+ /** Lazy mode only. Retention for the transform Lambda's log group (default 90 days). */
196
+ readonly transformLogRetention?: logs.RetentionDays;
197
+ /**
198
+ * Lazy mode only. Concurrency cap on the transform Lambda (default 10), a
199
+ * reservation that costs nothing idle. 0 disables transforms.
200
+ */
201
+ readonly transformReservedConcurrency?: number;
202
+ /**
203
+ * Lazy mode only. How long the Lambda's `assets/t/` outputs are kept
204
+ * (default 180 days), written as a lifecycle rule on a bucket this construct
205
+ * creates. On a BYO `bucket` nothing is written and the prop is required:
206
+ * passing it states that your bucket expires them itself, filtering on the
207
+ * tag `canopy-transform=lazy` so what `materialize-assets` writes survives,
208
+ * and on a versioned bucket also expiring noncurrent versions. Remove that
209
+ * rule before leaving lazy mode: a key the Lambda wrote first stays tagged
210
+ * after a build references it. A cross-account bucket's policy must also
211
+ * grant the transform role `s3:PutObjectTagging` on `assets/t/*`.
171
212
  */
172
213
  readonly transformOutputRetention?: Duration;
173
214
  /**
174
- * Execution role for the transform Lambda (default: CDK creates one).
215
+ * Lazy mode only. Execution role for the transform Lambda (default: CDK creates one).
175
216
  *
176
217
  * Set this when the role's ARN has to be computable WITHOUT a reference to
177
218
  * this construct - the motivating case is an asset bucket in a different AWS
@@ -192,11 +233,10 @@ export interface AssetSupportProps {
192
233
  */
193
234
  readonly transformRole?: iam.Role;
194
235
  /**
195
- * Name for the transform Lambda's CloudWatch log group (default:
236
+ * Lazy mode only. Name for the transform Lambda's CloudWatch log group (default:
196
237
  * `/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
238
+ * `/aws/lambda/<function-name>` - see the log group's comment in
239
+ * `buildTransformLambda` for why. Override to follow an
200
240
  * org naming convention, or when instantiating this construct twice in one
201
241
  * stack (the default name would collide).
202
242
  */
@@ -249,10 +289,8 @@ export declare const ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS: readonly ["assets", "as
249
289
  * ```ts
250
290
  * const behaviors = assetSupport.assetBehaviors()
251
291
  *
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.
292
+ * // CloudFront stops at the first matching pattern, so '/assets/t/*' MUST
293
+ * // come before '/assets/*', or lazy mode's misses never reach the Lambda.
256
294
  * new cloudfront.Distribution(this, 'Dist', {
257
295
  * defaultBehavior: ...,
258
296
  * additionalBehaviors: {
@@ -264,18 +302,13 @@ export declare const ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS: readonly ["assets", "as
264
302
  */
265
303
  export interface AssetCloudFrontBehaviors {
266
304
  /**
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.
305
+ * `/assets/*`: the S3 read origin (an origin group with `replicaBucket`).
271
306
  */
272
307
  readonly assets: cloudfront.BehaviorOptions;
273
308
  /**
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.
309
+ * `/assets/t/*`: the same read origin as `assets`, or with
310
+ * `lazyPublicTransforms` an origin group falling over to the transform
311
+ * Lambda on 403 or 404.
279
312
  */
280
313
  readonly assetsTransform: cloudfront.BehaviorOptions;
281
314
  }
@@ -312,15 +345,8 @@ export interface AssetUploadBehaviorRouteOptions extends AssetUploadBehaviorOpti
312
345
  * ```
313
346
  *
314
347
  * 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.
348
+ * the bucket and nothing else. Prefer the method when you already have an
349
+ * `AssetSupport`; both funnel into the same builder and cannot drift.
324
350
  *
325
351
  * No bucket CORS rule is written and none is needed: the edge supplies
326
352
  * `Access-Control-Allow-Origin`, and S3's ACCEPTANCE of a presigned POST is
@@ -336,12 +362,12 @@ export declare function assetUploadBehavior(scope: Construct, options: AssetUplo
336
362
  * Wires:
337
363
  *
338
364
  * - 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.
365
+ * - The public read path: `/assets/*` and `/assets/t/*` served from S3 (plus
366
+ * `replicaBucket` on 5xx). It computes nothing; derivatives come from
367
+ * `canopycms materialize-assets`.
368
+ * - With `lazyPublicTransforms`, the transform Lambda
369
+ * (`../../lambda/asset-transform/handler.ts`, built by `build:lambda`), its
370
+ * log group and its OAC-locked Function URL, behind `/assets/t/*`.
345
371
  * - `attachTo(distribution)`, which attaches the two CloudFront behaviors a
346
372
  * consuming distribution needs in the only safe order (see its doc comment
347
373
  * for why the order is the whole point); `assetBehaviors()` is the escape
@@ -356,11 +382,12 @@ export declare function assetUploadBehavior(scope: Construct, options: AssetUplo
356
382
  export declare class AssetSupport extends Construct {
357
383
  /** The bucket in use (either created here, or the BYO `props.bucket`). */
358
384
  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;
385
+ /** The lazy transform Lambda; undefined unless `lazyPublicTransforms`. */
386
+ readonly transformFunction: lambda.Function | undefined;
387
+ /** The transform Lambda's CloudWatch log group; undefined unless `lazyPublicTransforms`. */
388
+ readonly transformLogGroup: logs.LogGroup | undefined;
389
+ /** The transform Lambda's Function URL; undefined unless `lazyPublicTransforms`. */
390
+ readonly transformFunctionUrl: lambda.FunctionUrl | undefined;
364
391
  /** Effective advisory upload-size cap (see `AssetSupportProps.maxUploadBytes`). */
365
392
  readonly maxUploadBytes: number;
366
393
  private readonly behaviors;
@@ -388,6 +415,7 @@ export declare class AssetSupport extends Construct {
388
415
  */
389
416
  private uploadOrigin?;
390
417
  constructor(scope: Construct, id: string, props: AssetSupportProps);
418
+ private buildTransformLambda;
391
419
  private buildBehaviors;
392
420
  /**
393
421
  * The two CloudFront behavior configs this system needs.
@@ -424,9 +452,8 @@ export declare class AssetSupport extends Construct {
424
452
  * site's cookies and any cached basic-auth credential are live hazards
425
453
  * there, stripped by policy and function rather than never sent at all.
426
454
  * - 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;
455
+ * environment. That means one `AssetSupport`, so a `canopycms-cdk` bump
456
+ * would move every environment's asset pipeline at once. Only the upload route moves;
430
457
  * it depends on the bucket and nothing else, the same property that lets
431
458
  * `assetUploadBehavior` build it from a bucket alone.
432
459
  */
@@ -435,17 +462,12 @@ export declare class AssetSupport extends Construct {
435
462
  * Attach both asset behaviors to a concrete CloudFront distribution, in the
436
463
  * only safe order.
437
464
  *
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.
465
+ * CloudFront stops at the first matching pattern, and `/assets/*` also
466
+ * matches every `/assets/t/*` request. With `lazyPublicTransforms`, attaching
467
+ * it first means no miss ever reaches the transform Lambda, with no synth or
468
+ * deploy error; `'/assets/*'` also sorts first alphabetically. The two
469
+ * behaviors are identical otherwise, and the order is kept so that switching
470
+ * modes changes nothing else.
449
471
  *
450
472
  * `overrides` is merged into BOTH behaviors. The motivating case is a
451
473
  * distribution running a viewer-request function on every behavior - tier