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

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.
@@ -217,13 +217,17 @@ export declare class AssetSupport extends Construct {
217
217
  /** Effective advisory upload-size cap (see `AssetSupportProps.maxUploadBytes`). */
218
218
  readonly maxUploadBytes: number;
219
219
  private readonly behaviors;
220
- /** Distributions `attachTo` has already wired -- see that method's duplicate check. */
221
- private readonly attachedDistributions;
222
220
  constructor(scope: Construct, id: string, props: AssetSupportProps);
223
221
  private buildBehaviors;
224
222
  /**
225
223
  * The two CloudFront behavior configs this system needs.
226
224
  *
225
+ * If you use this rather than `attachTo` -- which now takes an `overrides`
226
+ * parameter, so needing per-behavior options is no longer a reason to fall
227
+ * back here -- you own the ordering, and you should assert it: read the
228
+ * SYNTHESIZED template's `CacheBehaviors` array index, not your own source
229
+ * object, since the property is about emitted order.
230
+ *
227
231
  * Prefer `attachTo(distribution)` or `CanopyCmsDistribution`'s
228
232
  * `assetSupport` prop, which attach these to a distribution in the only
229
233
  * safe order automatically. This method exists as the escape hatch for a
@@ -250,6 +254,33 @@ export declare class AssetSupport extends Construct {
250
254
  * 0x2A, `t` = 0x74), so alphabetizing the keys reproduces exactly this
251
255
  * failure, with no synth or deploy error to catch it.
252
256
  *
257
+ * `overrides` is merged into BOTH behaviors, and exists because without it
258
+ * this method is unusable by exactly the adopters who most need the ordering
259
+ * guarantee. A distribution that runs a viewer-request function on every
260
+ * behavior - tier basic-auth, most commonly - needs the asset behaviors to
261
+ * carry that same `functionAssociations`, or `/assets/*` is anonymously
262
+ * readable on an authenticated tier. Before this parameter existed such an
263
+ * adopter had to fall back to `assetBehaviors()` plus two hand-ordered
264
+ * `addBehavior` calls: the precise shape this method was added to eliminate,
265
+ * re-entered while believing ordering was handled upstream, which is worse
266
+ * than never having had the method. (`responseHeadersPolicy` is the same
267
+ * story for a repo with a shared security-headers policy.)
268
+ *
269
+ * Merged into both rather than per-behavior on purpose: applying one set to
270
+ * both is what preserves the ordering guarantee as the only thing this
271
+ * method decides. A caller who genuinely needs the two behaviors to differ
272
+ * has left this method's remit and should use `assetBehaviors()` - and keep
273
+ * their own ordering assertion.
274
+ *
275
+ * Typed `Partial<AddBehaviorOptions>` because that is exactly what
276
+ * `addBehavior(pattern, origin, behaviorOptions?)` accepts - `origin` is a
277
+ * POSITIONAL argument there, so `BehaviorOptions` (which is
278
+ * `AddBehaviorOptions` plus `origin`) would let a caller pass a key the call
279
+ * silently ignores. Measured, because the first version of this comment
280
+ * claimed the opposite: widening the parameter and passing an `origin`
281
+ * override changes nothing in the emitted template, so this narrowing
282
+ * prevents a confusing no-op rather than a broken origin group.
283
+ *
253
284
  * Needs a concrete `cloudfront.Distribution` - `addBehavior` is an instance
254
285
  * method on that class, not on `IDistribution` (what an imported/looked-up
255
286
  * distribution reference gives you). For a distribution built entirely
@@ -258,7 +289,7 @@ export declare class AssetSupport extends Construct {
258
289
  * directly instead and list `/assets/t/*` before `/assets/*` yourself - see
259
290
  * `AssetCloudFrontBehaviors`'s doc comment.
260
291
  */
261
- attachTo(distribution: cloudfront.Distribution): void;
292
+ attachTo(distribution: cloudfront.Distribution, overrides?: Partial<cloudfront.AddBehaviorOptions>): void;
262
293
  /**
263
294
  * Grant the CMS/editor principal (the CMS Lambda's role, typically) the
264
295
  * exact permissions `S3AssetStore` (packages/canopycms/src/assets/store-s3.ts)
@@ -111,6 +111,24 @@ 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
+ * Distributions that already carry the asset behaviors, tracked at MODULE level
116
+ * rather than per instance.
117
+ *
118
+ * Per-instance (the first version of this) only caught the same `AssetSupport`
119
+ * attaching twice. Two `AssetSupport` constructs attaching to one distribution
120
+ * slipped through and synthesized
121
+ * ['/assets/t/*','/assets/*','/assets/t/*','/assets/*'] - the identical
122
+ * duplicate-path-pattern deploy failure the guard exists to convert into a
123
+ * synth error. There is no legitimate form of that: both instances attach the
124
+ * same two patterns, so the second is always wrong regardless of which
125
+ * construct owns it.
126
+ *
127
+ * A `WeakSet` keyed on the distribution object, so it holds no reference that
128
+ * would outlive the construct tree and cannot leak across CDK apps in a test
129
+ * process.
130
+ */
131
+ const distributionsWithAssetBehaviors = new WeakSet();
114
132
  /**
115
133
  * The two CloudFront path patterns the asset system's behaviors are keyed
116
134
  * under, derived from `PREFIXES` above (the single source of truth for these
@@ -156,8 +174,6 @@ export const ASSET_BEHAVIOR_SPREAD_MISTAKE_KEYS = ['assets', 'assetsTransform'];
156
174
  export class AssetSupport extends Construct {
157
175
  constructor(scope, id, props) {
158
176
  super(scope, id);
159
- /** Distributions `attachTo` has already wired -- see that method's duplicate check. */
160
- this.attachedDistributions = new WeakSet();
161
177
  // Fail closed before anything else: refuse to build a stack around a
162
178
  // transform Lambda whose code asset was never verified to contain the
163
179
  // linux/arm64 sharp binary. Without this, `pnpm test` (whose
@@ -346,6 +362,12 @@ export class AssetSupport extends Construct {
346
362
  /**
347
363
  * The two CloudFront behavior configs this system needs.
348
364
  *
365
+ * If you use this rather than `attachTo` -- which now takes an `overrides`
366
+ * parameter, so needing per-behavior options is no longer a reason to fall
367
+ * back here -- you own the ordering, and you should assert it: read the
368
+ * SYNTHESIZED template's `CacheBehaviors` array index, not your own source
369
+ * object, since the property is about emitted order.
370
+ *
349
371
  * Prefer `attachTo(distribution)` or `CanopyCmsDistribution`'s
350
372
  * `assetSupport` prop, which attach these to a distribution in the only
351
373
  * safe order automatically. This method exists as the escape hatch for a
@@ -374,6 +396,33 @@ export class AssetSupport extends Construct {
374
396
  * 0x2A, `t` = 0x74), so alphabetizing the keys reproduces exactly this
375
397
  * failure, with no synth or deploy error to catch it.
376
398
  *
399
+ * `overrides` is merged into BOTH behaviors, and exists because without it
400
+ * this method is unusable by exactly the adopters who most need the ordering
401
+ * guarantee. A distribution that runs a viewer-request function on every
402
+ * behavior - tier basic-auth, most commonly - needs the asset behaviors to
403
+ * carry that same `functionAssociations`, or `/assets/*` is anonymously
404
+ * readable on an authenticated tier. Before this parameter existed such an
405
+ * adopter had to fall back to `assetBehaviors()` plus two hand-ordered
406
+ * `addBehavior` calls: the precise shape this method was added to eliminate,
407
+ * re-entered while believing ordering was handled upstream, which is worse
408
+ * than never having had the method. (`responseHeadersPolicy` is the same
409
+ * story for a repo with a shared security-headers policy.)
410
+ *
411
+ * Merged into both rather than per-behavior on purpose: applying one set to
412
+ * both is what preserves the ordering guarantee as the only thing this
413
+ * method decides. A caller who genuinely needs the two behaviors to differ
414
+ * has left this method's remit and should use `assetBehaviors()` - and keep
415
+ * their own ordering assertion.
416
+ *
417
+ * Typed `Partial<AddBehaviorOptions>` because that is exactly what
418
+ * `addBehavior(pattern, origin, behaviorOptions?)` accepts - `origin` is a
419
+ * POSITIONAL argument there, so `BehaviorOptions` (which is
420
+ * `AddBehaviorOptions` plus `origin`) would let a caller pass a key the call
421
+ * silently ignores. Measured, because the first version of this comment
422
+ * claimed the opposite: widening the parameter and passing an `origin`
423
+ * override changes nothing in the emitted template, so this narrowing
424
+ * prevents a confusing no-op rather than a broken origin group.
425
+ *
377
426
  * Needs a concrete `cloudfront.Distribution` - `addBehavior` is an instance
378
427
  * method on that class, not on `IDistribution` (what an imported/looked-up
379
428
  * distribution reference gives you). For a distribution built entirely
@@ -382,7 +431,7 @@ export class AssetSupport extends Construct {
382
431
  * directly instead and list `/assets/t/*` before `/assets/*` yourself - see
383
432
  * `AssetCloudFrontBehaviors`'s doc comment.
384
433
  */
385
- attachTo(distribution) {
434
+ attachTo(distribution, overrides) {
386
435
  // `addBehavior` does not dedupe, and these calls bypass
387
436
  // `CanopyCmsDistribution`'s own synth-time guard because they run after
388
437
  // that distribution is constructed. So attaching twice -- passing the
@@ -391,18 +440,40 @@ export class AssetSupport extends Construct {
391
440
  // ['/assets/t/*','/assets/*','/assets/t/*','/assets/*'] and fails at
392
441
  // deploy time when CloudFront rejects the duplicate patterns. Refuse it
393
442
  // here instead, where the message can say which two routes collided.
394
- if (this.attachedDistributions.has(distribution)) {
443
+ if (distributionsWithAssetBehaviors.has(distribution)) {
395
444
  throw new Error(`AssetSupport: attachTo() was already called for this distribution. Each pattern ` +
396
445
  `would be attached twice and CloudFront rejects duplicate path patterns at deploy ` +
397
446
  `time. This usually means the distribution was given CanopyCmsDistribution's ` +
398
447
  `\`assetSupport\` prop (which calls attachTo for you) as well as an explicit ` +
399
448
  `attachTo() call -- keep one.`);
400
449
  }
401
- this.attachedDistributions.add(distribution);
450
+ distributionsWithAssetBehaviors.add(distribution);
451
+ // Drop explicitly-`undefined` keys before merging. A spread copies own
452
+ // enumerable keys INCLUDING ones whose value is undefined, so
453
+ // `{ ...transformRest, ...{ cachePolicy: undefined } }` deletes the
454
+ // construct's choice and lets CDK substitute its own default - which is a
455
+ // DIFFERENT default. Measured: `cachePolicy: undefined` swaps this
456
+ // behavior's custom policy (TRANSFORM_CACHE_MIN_TTL = 0, which exists
457
+ // solely to stop the oversized-output redirect loop documented above) for
458
+ // the managed CACHING_OPTIMIZED and its 1-second min TTL; and
459
+ // `viewerProtocolPolicy: undefined` downgrades BOTH behaviors from
460
+ // redirect-to-https to allow-all, serving assets over plain HTTP.
461
+ //
462
+ // This is not a contrived input - it is what forwarding optional props
463
+ // produces: `attachTo(dist, { cachePolicy: props.maybePolicy })` with the
464
+ // prop unset. It typechecks (every AddBehaviorOptions field is already
465
+ // optional), synthesizes and deploys clean.
466
+ const definedOverrides = Object.fromEntries(Object.entries(overrides ?? {}).filter(([, value]) => value !== undefined));
402
467
  const { origin: transformOrigin, ...transformRest } = this.behaviors.assetsTransform;
403
468
  const { origin: assetsOrigin, ...assetsRest } = this.behaviors.assets;
404
- distribution.addBehavior(ASSETS_TRANSFORM_PATH_PATTERN, transformOrigin, transformRest);
405
- distribution.addBehavior(ASSETS_PATH_PATTERN, assetsOrigin, assetsRest);
469
+ distribution.addBehavior(ASSETS_TRANSFORM_PATH_PATTERN, transformOrigin, {
470
+ ...transformRest,
471
+ ...definedOverrides,
472
+ });
473
+ distribution.addBehavior(ASSETS_PATH_PATTERN, assetsOrigin, {
474
+ ...assetsRest,
475
+ ...definedOverrides,
476
+ });
406
477
  }
407
478
  /**
408
479
  * Grant the CMS/editor principal (the CMS Lambda's role, typically) the
@@ -66,9 +66,15 @@ export interface CanopyCmsDistributionProps {
66
66
  * Attach `AssetSupport`'s CloudFront behaviors (`/assets/*` and
67
67
  * `/assets/t/*`) to the distribution this construct builds, in the only
68
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.
69
+ * order matters. Calls `assetSupport.attachTo(distribution)` for you right
70
+ * after construction, with nothing to forget.
71
+ *
72
+ * NOT fully equivalent to calling `attachTo` yourself: this path cannot pass
73
+ * that method's `overrides` argument. If the asset behaviors need
74
+ * per-behavior options - a viewer-request function for tier auth being the
75
+ * motivating case, since without it `/assets/*` is anonymously readable on
76
+ * an authenticated tier - drop this prop and call
77
+ * `assetSupport.attachTo(this.distribution, { ... })` after construction.
72
78
  *
73
79
  * Prefer this over passing `assetSupport.assetBehaviors()` through
74
80
  * `additionalBehaviors` by hand - that stays available as an escape hatch
@@ -20,7 +20,21 @@ import { ASSETS_PATH_PATTERN, ASSETS_TRANSFORM_PATH_PATTERN, ASSET_BEHAVIOR_SPRE
20
20
  function mergeBehaviors(defaults, caller, attachingAssetSupport) {
21
21
  if (!caller)
22
22
  return defaults;
23
- const uncollided = Object.fromEntries(Object.entries(defaults).filter(([pattern]) => !(pattern in caller)));
23
+ // Compare NORMALIZED keys here too. CloudFront treats a leading '/' as
24
+ // optional, so a caller passing `'_next/static/*'` did not displace this
25
+ // construct's own `'/_next/static/*'` default and both were emitted -- two
26
+ // behaviors CloudFront reads as the same pattern, which it rejects at
27
+ // deploy. Same reason the hazard checks below normalize.
28
+ const callerPatterns = new Set(Object.keys(caller).map(normalizePathPattern));
29
+ const uncollided = Object.fromEntries(Object.entries(defaults).filter(([pattern]) => !callerPatterns.has(normalizePathPattern(pattern))));
30
+ const normalizedCallerKeys = Object.keys(caller).map(normalizePathPattern);
31
+ const duplicateCallerPattern = normalizedCallerKeys.find((pattern, i) => normalizedCallerKeys.indexOf(pattern) !== i);
32
+ if (duplicateCallerPattern !== undefined) {
33
+ throw new Error(`CanopyCmsDistribution: additionalBehaviors has two keys that CloudFront reads as the ` +
34
+ `same path pattern (${JSON.stringify(duplicateCallerPattern)} with and without its ` +
35
+ `leading '/'). CloudFront rejects duplicate path patterns at deploy time. Keep one ` +
36
+ `spelling.`);
37
+ }
24
38
  const merged = { ...uncollided, ...caller };
25
39
  assertNoAssetBehaviorOrderingHazards(merged, attachingAssetSupport);
26
40
  return merged;
@@ -68,6 +82,7 @@ function mergeBehaviors(defaults, caller, attachingAssetSupport) {
68
82
  function normalizePathPattern(pattern) {
69
83
  return pattern.startsWith('/') ? pattern.slice(1) : pattern;
70
84
  }
85
+ /** See the three-hazard list documented above `normalizePathPattern`. */
71
86
  function assertNoAssetBehaviorOrderingHazards(merged, attachingAssetSupport) {
72
87
  const keys = Object.keys(merged).map(normalizePathPattern);
73
88
  const assetsPattern = normalizePathPattern(ASSETS_PATH_PATTERN);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canopycms-cdk",
3
- "version": "0.0.66-int.83",
3
+ "version": "0.0.66-int.84",
4
4
  "description": "AWS CDK constructs and EC2 worker for CanopyCMS deployment",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "peerDependencies": {
31
31
  "aws-cdk-lib": "^2.260.0",
32
32
  "constructs": "^10.0.0",
33
- "canopycms": "0.0.66-int.83"
33
+ "canopycms": "0.0.66-int.84"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "aws-cdk-lib": {
@@ -54,8 +54,8 @@
54
54
  "tsx": "^4.19.0",
55
55
  "typescript": "^5.6.3",
56
56
  "vitest": "^4.1.0",
57
- "canopycms": "0.0.66-int.83",
58
- "canopycms-auth-clerk": "0.0.66-int.83"
57
+ "canopycms": "0.0.66-int.84",
58
+ "canopycms-auth-clerk": "0.0.66-int.84"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "rm -rf dist && tsc && node ../../scripts/add-js-extensions.mjs dist",