canopycms-cdk 0.0.66-int.81 → 0.0.66-int.83

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 (41) hide show
  1. package/dist/constructs/asset-support.d.ts +85 -13
  2. package/dist/constructs/asset-support.js +87 -4
  3. package/dist/constructs/cms-distribution.d.ts +34 -0
  4. package/dist/constructs/cms-distribution.js +107 -3
  5. package/dist/constructs/cms-service.d.ts +50 -1
  6. package/dist/constructs/cms-service.js +134 -4
  7. package/lambda/asset-transform/dist/handler.js +3 -0
  8. package/lambda/asset-transform/dist/node_modules/.package-lock.json +39 -39
  9. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/glib-2.0/include/glibconfig.h +1 -1
  10. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/{libvips-cpp.so.8.18.3 → libvips-cpp.so.8.18.6} +0 -0
  11. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/package.json +2 -2
  12. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/versions.json +13 -13
  13. package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/index.cjs +1 -1
  14. package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/lib/{sharp-linux-arm64-0.35.3.node → sharp-linux-arm64-0.35.4.node} +0 -0
  15. package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/package.json +2 -2
  16. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/index.cjs +1 -1
  17. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/sharp-wasm32-0.35.4.node.js +1 -0
  18. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/{sharp-wasm32-0.35.3.node.wasm → sharp-wasm32-0.35.4.node.wasm} +0 -0
  19. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/package.json +2 -2
  20. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/versions.json +9 -9
  21. package/lambda/asset-transform/dist/node_modules/sharp/dist/composite.cjs +6 -6
  22. package/lambda/asset-transform/dist/node_modules/sharp/dist/composite.mjs +6 -6
  23. package/lambda/asset-transform/dist/node_modules/sharp/dist/constructor.cjs +1 -1
  24. package/lambda/asset-transform/dist/node_modules/sharp/dist/constructor.mjs +1 -1
  25. package/lambda/asset-transform/dist/node_modules/sharp/dist/index.d.cts +2 -2
  26. package/lambda/asset-transform/dist/node_modules/sharp/dist/index.d.mts +2 -2
  27. package/lambda/asset-transform/dist/node_modules/sharp/dist/input.cjs +20 -15
  28. package/lambda/asset-transform/dist/node_modules/sharp/dist/input.mjs +20 -15
  29. package/lambda/asset-transform/dist/node_modules/sharp/dist/operation.cjs +4 -0
  30. package/lambda/asset-transform/dist/node_modules/sharp/dist/operation.mjs +4 -0
  31. package/lambda/asset-transform/dist/node_modules/sharp/dist/output.cjs +4 -7
  32. package/lambda/asset-transform/dist/node_modules/sharp/dist/output.mjs +4 -7
  33. package/lambda/asset-transform/dist/node_modules/sharp/dist/resize.cjs +8 -8
  34. package/lambda/asset-transform/dist/node_modules/sharp/dist/resize.mjs +8 -8
  35. package/lambda/asset-transform/dist/node_modules/sharp/lib/index.d.ts +2 -2
  36. package/lambda/asset-transform/dist/node_modules/sharp/package.json +38 -38
  37. package/lambda/asset-transform/dist/node_modules/sharp/src/common.cc +7 -4
  38. package/lambda/asset-transform/dist/node_modules/sharp/src/common.h +2 -2
  39. package/package.json +4 -4
  40. package/worker/dist/index.js +4497 -2689
  41. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/sharp-wasm32-0.35.3.node.js +0 -1
@@ -107,16 +107,54 @@ export interface AssetSupportProps {
107
107
  */
108
108
  readonly transformLogGroupName?: string;
109
109
  }
110
+ /**
111
+ * The two CloudFront path patterns the asset system's behaviors are keyed
112
+ * under, derived from `PREFIXES` above (the single source of truth for these
113
+ * prefixes in this file) rather than spelled out again. Exported so
114
+ * `cms-distribution.ts`'s synth-time ordering guard can recognize them
115
+ * without a second, driftable copy of the literal strings.
116
+ */
117
+ export declare const ASSETS_PATH_PATTERN: string;
118
+ export declare const ASSETS_TRANSFORM_PATH_PATTERN: string;
119
+ /**
120
+ * The literal property names of `AssetCloudFrontBehaviors`
121
+ * (`assets`/`assetsTransform`). It is a natural but broken mistake to spread
122
+ * `assetBehaviors()`'s return value directly into `additionalBehaviors` (a
123
+ * `Record<pathPattern, BehaviorOptions>`) - that type-checks and deploys
124
+ * clean, but synthesizes two CloudFront behaviors matching the literal path
125
+ * patterns `assets` and `assetsTransform`, which nothing ever requests,
126
+ * instead of the real `/assets/*` and `/assets/t/*` patterns. Use
127
+ * `attachTo()` (below) or `CanopyCmsDistribution`'s `assetSupport` prop
128
+ * instead. Exported so `cms-distribution.ts`'s synth-time guard can
129
+ * recognize the mistake and name it in its error.
130
+ */
131
+ export declare const ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS: readonly ["assets", "assetsTransform"];
110
132
  /**
111
133
  * The two CloudFront behavior configs the asset system needs, keyed by the
112
134
  * path pattern they belong under. Each value is a full `BehaviorOptions`
113
- * (origin included), so a consumer can use it either way:
135
+ * (origin included).
136
+ *
137
+ * Prefer `AssetSupport.attachTo(distribution)` or `CanopyCmsDistribution`'s
138
+ * `assetSupport` prop over consuming this directly - both encode the
139
+ * required attachment order in exactly one place instead of asking every
140
+ * caller to reproduce it correctly. This return value remains useful as an
141
+ * ESCAPE HATCH for a bespoke `new cloudfront.Distribution(...)` assembled
142
+ * entirely inline (its `additionalBehaviors` is fixed at construction, so
143
+ * there is no distribution yet to call `addBehavior` on) - but manual use
144
+ * must still preserve the ordering shown below, and
145
+ * `CanopyCmsDistribution`'s synth-time guard (`mergeBehaviors`) actively
146
+ * rejects three ways this goes wrong: `/assets/*` listed before
147
+ * `/assets/t/*`; the literal keys `assets`/`assetsTransform` (see
148
+ * `ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS`) from spreading this object directly
149
+ * into a `Record`; or an asset pattern listed here at all while
150
+ * `CanopyCmsDistribution`'s `assetSupport` prop is also passed. See
151
+ * `assertNoAssetBehaviorOrderingHazards`'s doc comment for the full list.
114
152
  *
115
153
  * ```ts
116
154
  * const behaviors = assetSupport.assetBehaviors()
117
155
  *
118
- * // Building a new distribution. CloudFront matches path patterns in the
119
- * // order they're listed and stops at the first match, so the more
156
+ * // Building a new distribution inline. CloudFront matches path patterns in
157
+ * // the order they're listed and stops at the first match, so the more
120
158
  * // specific '/assets/t/*' MUST come before '/assets/*' - otherwise the
121
159
  * // broader S3-only pattern swallows transform requests first and they
122
160
  * // 403 with no Lambda fallback.
@@ -127,12 +165,6 @@ export interface AssetSupportProps {
127
165
  * '/assets/*': behaviors.assets,
128
166
  * },
129
167
  * })
130
- *
131
- * // Adding to an existing distribution: the same first-match-wins ordering
132
- * // applies to the resulting CacheBehaviors list, so call addBehavior for
133
- * // '/assets/t/*' before '/assets/*' here too.
134
- * distribution.addBehavior('/assets/t/*', behaviors.assetsTransform.origin, behaviors.assetsTransform)
135
- * distribution.addBehavior('/assets/*', behaviors.assets.origin, behaviors.assets)
136
168
  * ```
137
169
  */
138
170
  export interface AssetCloudFrontBehaviors {
@@ -165,9 +197,11 @@ export interface AssetCloudFrontBehaviors {
165
197
  * (custom name/retention/removal policy instead of the
166
198
  * CloudFormation-implicit `/aws/lambda/<function-name>` group), and its
167
199
  * OAC-locked Function URL.
168
- * - `assetBehaviors()`, the two CloudFront behavior configs a consuming
169
- * distribution attaches (see `AssetCloudFrontBehaviors`'s doc comment for
170
- * both attachment shapes).
200
+ * - `attachTo(distribution)`, which attaches the two CloudFront behaviors a
201
+ * consuming distribution needs in the only safe order (see its doc comment
202
+ * for why the order is the whole point); `assetBehaviors()` is the escape
203
+ * hatch for a distribution built entirely by hand (see
204
+ * `AssetCloudFrontBehaviors`'s doc comment).
171
205
  * - `grantUploadAccess()`, the exact prefix-scoped grants the CMS/editor
172
206
  * principal needs to run `S3AssetStore` (packages/canopycms/src/assets/store-s3.ts).
173
207
  */
@@ -183,10 +217,48 @@ export declare class AssetSupport extends Construct {
183
217
  /** Effective advisory upload-size cap (see `AssetSupportProps.maxUploadBytes`). */
184
218
  readonly maxUploadBytes: number;
185
219
  private readonly behaviors;
220
+ /** Distributions `attachTo` has already wired -- see that method's duplicate check. */
221
+ private readonly attachedDistributions;
186
222
  constructor(scope: Construct, id: string, props: AssetSupportProps);
187
223
  private buildBehaviors;
188
- /** The two CloudFront behavior configs this system needs - see `AssetCloudFrontBehaviors`'s doc comment for both attachment shapes. */
224
+ /**
225
+ * The two CloudFront behavior configs this system needs.
226
+ *
227
+ * Prefer `attachTo(distribution)` or `CanopyCmsDistribution`'s
228
+ * `assetSupport` prop, which attach these to a distribution in the only
229
+ * safe order automatically. This method exists as the escape hatch for a
230
+ * distribution assembled entirely by hand - see `AssetCloudFrontBehaviors`'s
231
+ * doc comment for that shape and its ordering requirements.
232
+ */
189
233
  assetBehaviors(): AssetCloudFrontBehaviors;
234
+ /**
235
+ * Attach both asset behaviors to a concrete CloudFront distribution, in the
236
+ * only safe order.
237
+ *
238
+ * THIS METHOD EXISTS BECAUSE THE ORDER IS THE WHOLE POINT.
239
+ * `assetBehaviors()` returns `{ assets, assetsTransform }` with no path
240
+ * pattern attached at all (see that method's and `AssetCloudFrontBehaviors`'s
241
+ * doc comments) - so nothing stops a caller from attaching the two in
242
+ * either order. CloudFront matches path patterns in the order given and
243
+ * stops at the first match. `/assets/*` is a broader, S3-only pattern that
244
+ * also matches every `/assets/t/*` request; `/assets/t/*` is an origin
245
+ * group that fails over to the transform Lambda on a miss. Attach
246
+ * `/assets/*` first and every never-yet-computed transform gets a
247
+ * permanent 403 (an OAC-signed S3 miss reports 403) while already-computed
248
+ * transforms keep working - silent, launch-delayed, and permanent.
249
+ * `'/assets/*'` also sorts BEFORE `'/assets/t/*'` lexicographically (`*` =
250
+ * 0x2A, `t` = 0x74), so alphabetizing the keys reproduces exactly this
251
+ * failure, with no synth or deploy error to catch it.
252
+ *
253
+ * Needs a concrete `cloudfront.Distribution` - `addBehavior` is an instance
254
+ * method on that class, not on `IDistribution` (what an imported/looked-up
255
+ * distribution reference gives you). For a distribution built entirely
256
+ * inline (its `additionalBehaviors` fixed at construction, with no
257
+ * distribution yet to call `addBehavior` on), call `assetBehaviors()`
258
+ * directly instead and list `/assets/t/*` before `/assets/*` yourself - see
259
+ * `AssetCloudFrontBehaviors`'s doc comment.
260
+ */
261
+ attachTo(distribution: cloudfront.Distribution): void;
190
262
  /**
191
263
  * Grant the CMS/editor principal (the CMS Lambda's role, typically) the
192
264
  * exact permissions `S3AssetStore` (packages/canopycms/src/assets/store-s3.ts)
@@ -111,6 +111,28 @@ const TRANSFORM_OUTPUT_RETENTION = Duration.days(180);
111
111
  const TRANSFORM_CACHE_MIN_TTL = Duration.seconds(0);
112
112
  const TRANSFORM_CACHE_DEFAULT_TTL = Duration.days(1);
113
113
  const TRANSFORM_CACHE_MAX_TTL = Duration.days(365);
114
+ /**
115
+ * The two CloudFront path patterns the asset system's behaviors are keyed
116
+ * under, derived from `PREFIXES` above (the single source of truth for these
117
+ * prefixes in this file) rather than spelled out again. Exported so
118
+ * `cms-distribution.ts`'s synth-time ordering guard can recognize them
119
+ * without a second, driftable copy of the literal strings.
120
+ */
121
+ export const ASSETS_PATH_PATTERN = `/${PREFIXES.public}/*`;
122
+ export const ASSETS_TRANSFORM_PATH_PATTERN = `/${PREFIXES.transform}/*`;
123
+ /**
124
+ * The literal property names of `AssetCloudFrontBehaviors`
125
+ * (`assets`/`assetsTransform`). It is a natural but broken mistake to spread
126
+ * `assetBehaviors()`'s return value directly into `additionalBehaviors` (a
127
+ * `Record<pathPattern, BehaviorOptions>`) - that type-checks and deploys
128
+ * clean, but synthesizes two CloudFront behaviors matching the literal path
129
+ * patterns `assets` and `assetsTransform`, which nothing ever requests,
130
+ * instead of the real `/assets/*` and `/assets/t/*` patterns. Use
131
+ * `attachTo()` (below) or `CanopyCmsDistribution`'s `assetSupport` prop
132
+ * instead. Exported so `cms-distribution.ts`'s synth-time guard can
133
+ * recognize the mistake and name it in its error.
134
+ */
135
+ export const ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS = ['assets', 'assetsTransform'];
114
136
  /**
115
137
  * Per-site CDK construct for the asset/media delivery system (see
116
138
  * `.claude/future-tasks/assets-media-system.md` for the full design record).
@@ -123,15 +145,19 @@ const TRANSFORM_CACHE_MAX_TTL = Duration.days(365);
123
145
  * (custom name/retention/removal policy instead of the
124
146
  * CloudFormation-implicit `/aws/lambda/<function-name>` group), and its
125
147
  * OAC-locked Function URL.
126
- * - `assetBehaviors()`, the two CloudFront behavior configs a consuming
127
- * distribution attaches (see `AssetCloudFrontBehaviors`'s doc comment for
128
- * both attachment shapes).
148
+ * - `attachTo(distribution)`, which attaches the two CloudFront behaviors a
149
+ * consuming distribution needs in the only safe order (see its doc comment
150
+ * for why the order is the whole point); `assetBehaviors()` is the escape
151
+ * hatch for a distribution built entirely by hand (see
152
+ * `AssetCloudFrontBehaviors`'s doc comment).
129
153
  * - `grantUploadAccess()`, the exact prefix-scoped grants the CMS/editor
130
154
  * principal needs to run `S3AssetStore` (packages/canopycms/src/assets/store-s3.ts).
131
155
  */
132
156
  export class AssetSupport extends Construct {
133
157
  constructor(scope, id, props) {
134
158
  super(scope, id);
159
+ /** Distributions `attachTo` has already wired -- see that method's duplicate check. */
160
+ this.attachedDistributions = new WeakSet();
135
161
  // Fail closed before anything else: refuse to build a stack around a
136
162
  // transform Lambda whose code asset was never verified to contain the
137
163
  // linux/arm64 sharp binary. Without this, `pnpm test` (whose
@@ -317,10 +343,67 @@ export class AssetSupport extends Construct {
317
343
  };
318
344
  return { assets, assetsTransform };
319
345
  }
320
- /** The two CloudFront behavior configs this system needs - see `AssetCloudFrontBehaviors`'s doc comment for both attachment shapes. */
346
+ /**
347
+ * The two CloudFront behavior configs this system needs.
348
+ *
349
+ * Prefer `attachTo(distribution)` or `CanopyCmsDistribution`'s
350
+ * `assetSupport` prop, which attach these to a distribution in the only
351
+ * safe order automatically. This method exists as the escape hatch for a
352
+ * distribution assembled entirely by hand - see `AssetCloudFrontBehaviors`'s
353
+ * doc comment for that shape and its ordering requirements.
354
+ */
321
355
  assetBehaviors() {
322
356
  return this.behaviors;
323
357
  }
358
+ /**
359
+ * Attach both asset behaviors to a concrete CloudFront distribution, in the
360
+ * only safe order.
361
+ *
362
+ * THIS METHOD EXISTS BECAUSE THE ORDER IS THE WHOLE POINT.
363
+ * `assetBehaviors()` returns `{ assets, assetsTransform }` with no path
364
+ * pattern attached at all (see that method's and `AssetCloudFrontBehaviors`'s
365
+ * doc comments) - so nothing stops a caller from attaching the two in
366
+ * either order. CloudFront matches path patterns in the order given and
367
+ * stops at the first match. `/assets/*` is a broader, S3-only pattern that
368
+ * also matches every `/assets/t/*` request; `/assets/t/*` is an origin
369
+ * group that fails over to the transform Lambda on a miss. Attach
370
+ * `/assets/*` first and every never-yet-computed transform gets a
371
+ * permanent 403 (an OAC-signed S3 miss reports 403) while already-computed
372
+ * transforms keep working - silent, launch-delayed, and permanent.
373
+ * `'/assets/*'` also sorts BEFORE `'/assets/t/*'` lexicographically (`*` =
374
+ * 0x2A, `t` = 0x74), so alphabetizing the keys reproduces exactly this
375
+ * failure, with no synth or deploy error to catch it.
376
+ *
377
+ * Needs a concrete `cloudfront.Distribution` - `addBehavior` is an instance
378
+ * method on that class, not on `IDistribution` (what an imported/looked-up
379
+ * distribution reference gives you). For a distribution built entirely
380
+ * inline (its `additionalBehaviors` fixed at construction, with no
381
+ * distribution yet to call `addBehavior` on), call `assetBehaviors()`
382
+ * directly instead and list `/assets/t/*` before `/assets/*` yourself - see
383
+ * `AssetCloudFrontBehaviors`'s doc comment.
384
+ */
385
+ attachTo(distribution) {
386
+ // `addBehavior` does not dedupe, and these calls bypass
387
+ // `CanopyCmsDistribution`'s own synth-time guard because they run after
388
+ // that distribution is constructed. So attaching twice -- passing the
389
+ // `assetSupport` prop AND calling this yourself, most likely, since the
390
+ // prop's doc comment describes them as equivalent -- synthesizes
391
+ // ['/assets/t/*','/assets/*','/assets/t/*','/assets/*'] and fails at
392
+ // deploy time when CloudFront rejects the duplicate patterns. Refuse it
393
+ // here instead, where the message can say which two routes collided.
394
+ if (this.attachedDistributions.has(distribution)) {
395
+ throw new Error(`AssetSupport: attachTo() was already called for this distribution. Each pattern ` +
396
+ `would be attached twice and CloudFront rejects duplicate path patterns at deploy ` +
397
+ `time. This usually means the distribution was given CanopyCmsDistribution's ` +
398
+ `\`assetSupport\` prop (which calls attachTo for you) as well as an explicit ` +
399
+ `attachTo() call -- keep one.`);
400
+ }
401
+ this.attachedDistributions.add(distribution);
402
+ const { origin: transformOrigin, ...transformRest } = this.behaviors.assetsTransform;
403
+ const { origin: assetsOrigin, ...assetsRest } = this.behaviors.assets;
404
+ distribution.addBehavior(ASSETS_TRANSFORM_PATH_PATTERN, transformOrigin, transformRest);
405
+ distribution.addBehavior(ASSETS_PATH_PATTERN, assetsOrigin, assetsRest);
406
+ }
324
407
  /**
325
408
  * Grant the CMS/editor principal (the CMS Lambda's role, typically) the
326
409
  * exact permissions `S3AssetStore` (packages/canopycms/src/assets/store-s3.ts)
@@ -1,5 +1,6 @@
1
1
  import { Construct } from 'constructs';
2
2
  import { Duration, aws_cloudfront as cloudfront, aws_certificatemanager as acm, aws_route53 as route53, aws_lambda as lambda } from 'aws-cdk-lib';
3
+ import type { AssetSupport } from './asset-support.js';
3
4
  export interface CanopyCmsDistributionProps {
4
5
  /** Lambda Function URL from CanopyCmsService */
5
6
  functionUrl: lambda.FunctionUrl;
@@ -49,8 +50,39 @@ export interface CanopyCmsDistributionProps {
49
50
  * overridden key takes YOUR position in this object, not the position the
50
51
  * default held. (A plain object spread would do the opposite; see
51
52
  * `mergeBehaviors`.)
53
+ *
54
+ * Prefer the `assetSupport` prop over wiring `AssetSupport`'s behaviors in
55
+ * here by hand — but if you do it by hand anyway, `mergeBehaviors` throws
56
+ * at synth if it sees any of the three AssetSupport footguns: `/assets/*`
57
+ * listed before `/assets/t/*`; the literal keys `assets`/`assetsTransform`
58
+ * (from spreading `assetBehaviors()`'s return value directly into this
59
+ * object instead of keying it by path pattern); or an asset pattern listed
60
+ * here at all while the `assetSupport` prop is also passed (see that
61
+ * prop's own doc comment). See `assertNoAssetBehaviorOrderingHazards`'s
62
+ * doc comment for the full list.
52
63
  */
53
64
  additionalBehaviors?: Record<string, cloudfront.BehaviorOptions>;
65
+ /**
66
+ * Attach `AssetSupport`'s CloudFront behaviors (`/assets/*` and
67
+ * `/assets/t/*`) to the distribution this construct builds, in the only
68
+ * safe order - see `AssetSupport.attachTo()`'s doc comment for why the
69
+ * order matters. Equivalent to calling
70
+ * `assetSupport.attachTo(distribution)` yourself right after construction,
71
+ * but with nothing to forget.
72
+ *
73
+ * Prefer this over passing `assetSupport.assetBehaviors()` through
74
+ * `additionalBehaviors` by hand - that stays available as an escape hatch
75
+ * (e.g. for behaviors that are not from `AssetSupport` at all), and this
76
+ * construct's synth-time guard (`mergeBehaviors`) still checks it for the
77
+ * three ways that manual wiring is known to go wrong: see
78
+ * `additionalBehaviors`'s own doc comment.
79
+ *
80
+ * No ordering logic lives in this construct beyond calling this method -
81
+ * ordering is encoded exactly once, in `AssetSupport.attachTo()` itself.
82
+ *
83
+ * @default - no asset behaviors are attached
84
+ */
85
+ assetSupport?: AssetSupport;
54
86
  }
55
87
  /**
56
88
  * Optional CDK construct for CanopyCMS CloudFront distribution.
@@ -66,6 +98,8 @@ export interface CanopyCmsDistributionProps {
66
98
  * - Cache policies: no-cache for /api/* and /edit*, cache /_next/static/*
67
99
  * - Viewer-request CloudFront Function setting x-forwarded-host (redirect-URL
68
100
  * derivation behind the Host-stripping OAC origin)
101
+ * - `AssetSupport`'s `/assets/*` and `/assets/t/*` behaviors, in the required
102
+ * order, when the `assetSupport` prop is passed
69
103
  */
70
104
  export declare class CanopyCmsDistribution extends Construct {
71
105
  /** The CloudFront distribution */
@@ -1,6 +1,7 @@
1
1
  import { Construct } from 'constructs';
2
2
  import { Duration, Stack, Token, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_certificatemanager as acm, aws_route53 as route53, aws_route53_targets as targets, } from 'aws-cdk-lib';
3
3
  import { DEFAULT_CMS_LAMBDA_TIMEOUT, MAX_CLOUDFRONT_ORIGIN_READ_TIMEOUT } from './cms-service.js';
4
+ import { ASSETS_PATH_PATTERN, ASSETS_TRANSFORM_PATH_PATTERN, ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS, } from './asset-support.js';
4
5
  /**
5
6
  * Merge caller behaviors over this construct's own, preserving THE CALLER'S
6
7
  * ordering for every key they supply.
@@ -16,11 +17,105 @@ import { DEFAULT_CMS_LAMBDA_TIMEOUT, MAX_CLOUDFRONT_ORIGIN_READ_TIMEOUT } from '
16
17
  * Dropping collided defaults FIRST means the caller's own object contributes
17
18
  * its keys in its own order.
18
19
  */
19
- function mergeBehaviors(defaults, caller) {
20
+ function mergeBehaviors(defaults, caller, attachingAssetSupport) {
20
21
  if (!caller)
21
22
  return defaults;
22
23
  const uncollided = Object.fromEntries(Object.entries(defaults).filter(([pattern]) => !(pattern in caller)));
23
- return { ...uncollided, ...caller };
24
+ const merged = { ...uncollided, ...caller };
25
+ assertNoAssetBehaviorOrderingHazards(merged, attachingAssetSupport);
26
+ return merged;
27
+ }
28
+ /**
29
+ * Guards against the three AssetSupport CloudFront-behavior hazards a manually
30
+ * assembled `additionalBehaviors` can introduce (see
31
+ * `AssetSupport.attachTo()`'s doc comment for the full mechanism):
32
+ *
33
+ * 1. The literal keys `assets`/`assetsTransform` present in `merged` mean
34
+ * `assetSupport.assetBehaviors()`'s return value was spread directly into
35
+ * `additionalBehaviors` (a `Record<pathPattern, BehaviorOptions>`) instead
36
+ * of being attached via `assetSupport.attachTo(distribution)` or this
37
+ * construct's `assetSupport` prop. This type-checks and deploys clean,
38
+ * synthesizing two behaviors matching the literal path patterns `assets`
39
+ * and `assetsTransform`, which nothing ever requests.
40
+ * 2. `/assets/*` listed before `/assets/t/*` means CloudFront's first-match-
41
+ * wins ordering serves every transform request off the broader, S3-only
42
+ * `/assets/*` behavior, permanently 403ing any derivative that has not
43
+ * already been computed (an OAC-signed S3 miss reports 403).
44
+ * 3. Either asset pattern present in `merged` WHILE the `assetSupport` prop is
45
+ * also passed means both wiring routes are active at once, so each pattern
46
+ * is attached twice. This is the migration mistake specifically - keeping a
47
+ * hand-wired block while adopting the prop - and hazard 2 cannot catch it,
48
+ * because the hand-written order is usually the correct one.
49
+ *
50
+ * `Object.keys` insertion order is spec-guaranteed for string keys like
51
+ * these (none are integer-like array-index strings), so comparing index
52
+ * order here is sound.
53
+ *
54
+ * SCOPE LIMIT: this protects only callers going through
55
+ * `CanopyCmsDistribution`'s own `additionalBehaviors` merge. It cannot help a
56
+ * bespoke `new cloudfront.Distribution(...)` assembled elsewhere -
57
+ * `AssetSupport.attachTo()` is the answer there.
58
+ */
59
+ /**
60
+ * CloudFront treats the leading `/` on a path pattern as optional -- `assets/*`
61
+ * and `/assets/*` match exactly the same requests, and AWS's own console and
62
+ * docs frequently show the slash-less spelling. The checks below therefore
63
+ * compare NORMALIZED keys: without this, writing `{ 'assets/*': ..., 'assets/t/*': ... }`
64
+ * walked past all three hazards and synthesized the broad-pattern-first order
65
+ * this guard exists to refuse -- silently, which is the worst of the failure
66
+ * modes here.
67
+ */
68
+ function normalizePathPattern(pattern) {
69
+ return pattern.startsWith('/') ? pattern.slice(1) : pattern;
70
+ }
71
+ function assertNoAssetBehaviorOrderingHazards(merged, attachingAssetSupport) {
72
+ const keys = Object.keys(merged).map(normalizePathPattern);
73
+ const assetsPattern = normalizePathPattern(ASSETS_PATH_PATTERN);
74
+ const transformPattern = normalizePathPattern(ASSETS_TRANSFORM_PATH_PATTERN);
75
+ const spreadMistakeKeys = ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS.filter((key) => key in merged);
76
+ if (spreadMistakeKeys.length > 0) {
77
+ throw new Error(`CanopyCmsDistribution: additionalBehaviors has literal key(s) ` +
78
+ `${spreadMistakeKeys.map((k) => JSON.stringify(k)).join(', ')}, which is not a ` +
79
+ `CloudFront path pattern. This usually means assetSupport.assetBehaviors()'s return ` +
80
+ `value was spread directly into additionalBehaviors instead of attached via ` +
81
+ `assetSupport.attachTo(distribution) or this construct's \`assetSupport\` prop - either ` +
82
+ `of which attaches the real '${ASSETS_PATH_PATTERN}' and '${ASSETS_TRANSFORM_PATH_PATTERN}' ` +
83
+ `path patterns in the required order.`);
84
+ }
85
+ // Both halves wired at once. `attachTo` runs AFTER the distribution is
86
+ // constructed, so its `addBehavior` calls never pass through this function -
87
+ // meaning a caller who keeps a hand-wired asset block AND adopts the
88
+ // `assetSupport` prop gets each pattern attached twice. Measured: CDK does
89
+ // not object, and synthesizes CacheBehaviors
90
+ // ['/assets/t/*','/assets/*','/assets/t/*','/assets/*'], which CloudFront
91
+ // then rejects at deploy time for the duplicate path patterns. That is a
92
+ // late failure of exactly the kind this guard exists to convert into a synth
93
+ // one, and it is the most likely mistake during migration: the correct
94
+ // hand-written order does NOT trip the check below, so nothing else would
95
+ // catch it.
96
+ if (attachingAssetSupport) {
97
+ const alsoWiredByHand = [ASSETS_PATH_PATTERN, ASSETS_TRANSFORM_PATH_PATTERN].filter((pattern) => keys.includes(normalizePathPattern(pattern)));
98
+ if (alsoWiredByHand.length > 0) {
99
+ throw new Error(`CanopyCmsDistribution: the \`assetSupport\` prop attaches ` +
100
+ `'${ASSETS_TRANSFORM_PATH_PATTERN}' and '${ASSETS_PATH_PATTERN}' itself, but ` +
101
+ `additionalBehaviors already lists ${alsoWiredByHand.map((p) => `'${p}'`).join(' and ')}. ` +
102
+ `Each pattern would be attached twice and CloudFront rejects duplicate path patterns at ` +
103
+ `deploy time. Remove the asset entries from additionalBehaviors and keep the ` +
104
+ `\`assetSupport\` prop, which also guarantees the required order.`);
105
+ }
106
+ }
107
+ const assetsIndex = keys.indexOf(assetsPattern);
108
+ const transformIndex = keys.indexOf(transformPattern);
109
+ if (assetsIndex !== -1 && transformIndex !== -1 && assetsIndex < transformIndex) {
110
+ throw new Error(`CanopyCmsDistribution: additionalBehaviors lists '${ASSETS_PATH_PATTERN}' before ` +
111
+ `'${ASSETS_TRANSFORM_PATH_PATTERN}'. CloudFront matches path patterns in the order given ` +
112
+ `and stops at the first match, so every '${ASSETS_TRANSFORM_PATH_PATTERN}' request would ` +
113
+ `be served by the broader, S3-only '${ASSETS_PATH_PATTERN}' behavior and never fail over ` +
114
+ `to the transform Lambda - a permanent 403 on any derivative that has not already been ` +
115
+ `computed. List '${ASSETS_TRANSFORM_PATH_PATTERN}' first, or use ` +
116
+ `assetSupport.attachTo(distribution) / this construct's \`assetSupport\` prop, which get ` +
117
+ `the order right automatically.`);
118
+ }
24
119
  }
25
120
  /**
26
121
  * Optional CDK construct for CanopyCMS CloudFront distribution.
@@ -36,6 +131,8 @@ function mergeBehaviors(defaults, caller) {
36
131
  * - Cache policies: no-cache for /api/* and /edit*, cache /_next/static/*
37
132
  * - Viewer-request CloudFront Function setting x-forwarded-host (redirect-URL
38
133
  * derivation behind the Host-stripping OAC origin)
134
+ * - `AssetSupport`'s `/assets/*` and `/assets/t/*` behaviors, in the required
135
+ * order, when the `assetSupport` prop is passed
39
136
  */
40
137
  export class CanopyCmsDistribution extends Construct {
41
138
  constructor(scope, id, props) {
@@ -160,8 +257,15 @@ export class CanopyCmsDistribution extends Construct {
160
257
  cachePolicy: staticCachePolicy,
161
258
  viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
162
259
  },
163
- }, props.additionalBehaviors),
260
+ }, props.additionalBehaviors, props.assetSupport !== undefined),
164
261
  });
262
+ // Attach AssetSupport's behaviors (if provided), in the only safe order -
263
+ // see AssetSupport.attachTo()'s doc comment. This is the ONLY place this
264
+ // construct deals with the asset behaviors' ordering; the actual ordering
265
+ // logic lives exactly once, inside attachTo() itself. Callers who instead
266
+ // pass `assetSupport.assetBehaviors()` through `additionalBehaviors` by
267
+ // hand are covered by `mergeBehaviors`'s synth-time guard above instead.
268
+ props.assetSupport?.attachTo(this.distribution);
165
269
  // ========================================================================
166
270
  // DNS Records
167
271
  // ========================================================================
@@ -72,8 +72,57 @@ export interface CanopyCmsServiceProps {
72
72
  githubTokenSecretArn?: string;
73
73
  /** Secrets Manager ARN for the Clerk secret key */
74
74
  clerkSecretKeySecretArn?: string;
75
- /** Base branch name (default: 'main') */
75
+ /**
76
+ * The GitHub repository's default branch name (default: 'main').
77
+ *
78
+ * Interpolated straight into a git ref (`refs/heads/{baseBranch}`) and into
79
+ * the worker's `.env` heredoc, so an invalid value is rejected at synth (see
80
+ * `assertValidGitBranchName` above) instead of deploying an instance that
81
+ * boots successfully and then fails. And fail it will, permanently:
82
+ * `verifyBaseBranchExists`
83
+ * (packages/canopycms/src/worker/cms-worker.ts) throws when the named branch
84
+ * does not exist in the cloned `remote.git`, `start()`'s catch records the
85
+ * fatal error, `worker/index.ts` exits 1, and systemd's `Restart=always`
86
+ * repeats that forever — there is no working worker at all until the value
87
+ * is fixed and the instance replaced, and `rebaseActiveBranches` fetches and
88
+ * rebases against the wrong lineage in the meantime.
89
+ *
90
+ * MUST match the shared repo's `canopycms.config.ts`'s `defaultBaseBranch`
91
+ * (both default to 'main' when unset) — the two are resolved by different
92
+ * processes (this stamps the worker's `.env`; the Lambda reads
93
+ * `config.defaultBaseBranch` at request time) with no automatic
94
+ * reconciliation between them. `infrastructure/lib/cms-stack.ts`, as
95
+ * scaffolded by `canopycms init-deploy aws`, derives this prop FROM that same
96
+ * config file at synth time so the two cannot drift for adopters who deploy
97
+ * through the generated stack; a hand-rolled stack must set this explicitly
98
+ * whenever `defaultBaseBranch` is anything other than 'main'.
99
+ */
76
100
  baseBranch?: string;
101
+ /**
102
+ * Explicit settings-branch name, stamped into the worker's
103
+ * `CANOPYCMS_SETTINGS_BRANCH` environment variable (default: unset, in which
104
+ * case the worker falls through to the same computed name the Lambda uses,
105
+ * `canopycms-settings-<deploymentName>` — see `deploymentName` below).
106
+ *
107
+ * Only set this to mirror an adopter-configured `config.settingsBranch` in
108
+ * `canopycms.config.ts`. Leaving both unset (the default) is safe: Lambda and
109
+ * worker then resolve the SAME computed name independently, with nothing to
110
+ * keep in sync. But if the shared config sets `settingsBranch` explicitly
111
+ * and this prop is left unset, the worker keeps resolving the computed name
112
+ * while the Lambda's `getSettingsBranchName` short-circuits on
113
+ * `config.settingsBranch` — so the two own different branches. The PRIMARY
114
+ * settings-push path still reaches GitHub (its task payload carries the
115
+ * Lambda-resolved name), but the worker's per-cycle backstop push
116
+ * (`pushSettingsBranches`) targets the wrong branch and its "foreign settings
117
+ * branch" [SYNC-M3] warning misfires against the deployment's own branch.
118
+ * Validated at synth like `baseBranch` (see `assertValidGitBranchName`),
119
+ * since this is interpolated into a git ref and the worker's `.env` heredoc
120
+ * too. `infrastructure/lib/cms-stack.ts`, as scaffolded by
121
+ * `canopycms init-deploy aws`, derives this prop from the same
122
+ * `canopycms.config.ts` at synth time, so the two cannot drift for adopters
123
+ * who deploy through the generated stack.
124
+ */
125
+ settingsBranch?: string;
77
126
  /**
78
127
  * Deployment name (default: 'prod'). Namespaces the settings branch
79
128
  * (`canopycms-settings-{deploymentName}`) so this stack's CMS Lambda/worker