@reventlessdev/reventless-aws 3.0.0-alpha.239 → 3.0.0-alpha.241

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.
@@ -20,9 +20,20 @@
20
20
  // both cost and delete semantics and belongs to a deployment's judgement rather
21
21
  // than to a framework default.
22
22
  //
23
- // Follow-up, once a store's lifecycle follows a declaration rather than an
24
- // explicit line of app code: `protect` / `retainOnDelete`, so an accidental
25
- // removal of that declaration cannot destroy a bucket holding live objects.
23
+ // A store's lifecycle now follows a *declaration*, which changes the risk. A
24
+ // hand-written bucket could only be destroyed by editing a line of Pulumi; a
25
+ // declared one can be destroyed by renaming a field, because the last
26
+ // `@storageRef("productImages")` disappearing removes the requirement. Hence
27
+ // `~protect`, on by default and turned off only for stacks that are routinely
28
+ // torn down — see `Util_StoreLayout.protectionFor`, which decides that from
29
+ // disposability rather than from the store's layout.
30
+ //
31
+ // Not to be confused with the framework's other buckets: a **store** is
32
+ // declared by a field's type and holds values that events reference, so its
33
+ // objects outlive any single deploy. A **task bucket** (`TaskBucket_S3`) is
34
+ // wired by a slice and holds transient input, and the plugin-bundle and
35
+ // host-ui-bundle buckets are hosting substrate. Neither has a `@storageRef`
36
+ // field pointing at it, and neither is provisioned from a declaration.
26
37
 
27
38
  open PulumiAws
28
39
 
@@ -39,24 +50,62 @@ let defaultCorsRules: S3.Bucket.corsRules = [
39
50
  ]
40
51
 
41
52
  /** Create an object store bucket with framework attribution tags and an
42
- all-true public-access block. */
53
+ all-true public-access block.
54
+
55
+ `~keyPrefix` is the path this store's object keys are rooted at; it travels
56
+ in the returned record so the mint side and the serve side read one value
57
+ instead of restating it.
58
+
59
+ `~plugin` attributes the store to the plugin whose field declared it. The
60
+ bucket is created by the platform deploy — that is where the serving CDN and
61
+ the presign services live, and where a shared bucket has a single owner — so
62
+ ownership is expressed in the tags rather than in which stack ran `make`.
63
+
64
+ `~protect` (default on) blocks `pulumi destroy` and accidental replacement.
65
+ `~forceDestroy` is its counterpart for disposable stacks: a protected bucket
66
+ cannot be torn down, so a PR stack would leak exactly the buckets that
67
+ sharing exists to save. */
43
68
  let make = (
44
69
  ~name: string,
70
+ ~keyPrefix: string=Upload_Presign_S3.defaultServedPrefix,
71
+ ~plugin: option<string>=?,
72
+ ~protect: bool=true,
73
+ ~forceDestroy: bool=false,
45
74
  ~corsRules: S3.Bucket.corsRules=defaultCorsRules,
46
75
  ~opts: option<Pulumi.CustomResourceOptions.t>=?,
47
76
  ): ReventlessInfra.Platform.objectStore => {
77
+ // A declared store is plugin substrate; the legacy hand-written store, which
78
+ // names no plugin, stays platform-scoped so its tags are unchanged.
79
+ let (kind, scope) = switch plugin {
80
+ | Some(_) => (
81
+ ReventlessCore.ComponentType.Plugin,
82
+ ReventlessCore.ResourceAttribution.Scope.Plugin,
83
+ )
84
+ | None => (
85
+ ReventlessCore.ComponentType.Platform,
86
+ ReventlessCore.ResourceAttribution.Scope.Platform,
87
+ )
88
+ }
89
+
90
+ let opts: Pulumi.CustomResourceOptions.t = {
91
+ ...opts->Option.getOr({}),
92
+ protect,
93
+ }
94
+
48
95
  let bucket = S3.Bucket.make(
49
96
  ~name,
50
97
  ~args={
51
98
  corsRules: corsRules->Pulumi.Input.make,
99
+ forceDestroy: forceDestroy->Pulumi.Input.make,
52
100
  tags: AWS.Tags.make(
53
101
  ~name,
54
- ~kind=ReventlessCore.ComponentType.Platform,
102
+ ~kind,
55
103
  ~role=Other("ObjectStore"),
56
- ~scope=Platform,
104
+ ~scope,
105
+ ~plugin?,
57
106
  ),
58
107
  },
59
- ~opts?,
108
+ ~opts,
60
109
  )
61
110
 
62
111
  // A served store is read through the UI's CDN via an origin access control,
@@ -80,5 +129,18 @@ let make = (
80
129
  bucketId: bucket.id->Pulumi.Output.asInput,
81
130
  bucketArn: bucket.arn->Pulumi.Output.asInput,
82
131
  bucketRegionalDomainName: bucket.bucketRegionalDomainName->Pulumi.Output.asInput,
132
+ keyPrefix,
83
133
  }
84
134
  }
135
+
136
+ /** The same bucket, addressed under a different key prefix.
137
+
138
+ A shared-layout stack puts every declared store in one bucket, so the bucket
139
+ is created once and each store is that bucket seen through its own prefix.
140
+ Splitting this out keeps the memoisation of "one bucket per physical name"
141
+ separate from "one store per declaration", which are different cardinalities
142
+ the moment a layout shares. */
143
+ let underPrefix = (
144
+ store: ReventlessInfra.Platform.objectStore,
145
+ ~keyPrefix: string,
146
+ ): ReventlessInfra.Platform.objectStore => {...store, keyPrefix}
@@ -1,8 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
- import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as AWS_Tags$ReventlessAws from "../adapter/AWS_Tags.res.mjs";
6
+ import * as Upload_Presign_S3$ReventlessAws from "../adapter/Upload/Upload_Presign_S3.res.mjs";
6
7
 
7
8
  let defaultCorsRules = [{
8
9
  allowedHeaders: ["*"],
@@ -17,15 +18,28 @@ let defaultCorsRules = [{
17
18
  maxAgeSeconds: 3000
18
19
  }];
19
20
 
20
- function make(name, corsRulesOpt, opts) {
21
+ function make(name, keyPrefixOpt, plugin, protectOpt, forceDestroyOpt, corsRulesOpt, opts) {
22
+ let keyPrefix = keyPrefixOpt !== undefined ? keyPrefixOpt : Upload_Presign_S3$ReventlessAws.defaultServedPrefix;
23
+ let protect = protectOpt !== undefined ? protectOpt : true;
24
+ let forceDestroy = forceDestroyOpt !== undefined ? forceDestroyOpt : false;
21
25
  let corsRules = corsRulesOpt !== undefined ? corsRulesOpt : defaultCorsRules;
26
+ let match = plugin !== undefined ? [
27
+ "Plugin",
28
+ "Plugin"
29
+ ] : [
30
+ "Platform",
31
+ "Platform"
32
+ ];
33
+ let newrecord = {...Stdlib_Option.getOr(opts, {})};
34
+ newrecord.protect = protect;
22
35
  let bucket = new (Aws.s3.Bucket)(name, {
23
36
  corsRules: corsRules,
24
- tags: AWS_Tags$ReventlessAws.make(name, "Platform", {
37
+ forceDestroy: forceDestroy,
38
+ tags: AWS_Tags$ReventlessAws.make(name, match[0], {
25
39
  TAG: "Other",
26
40
  _0: "ObjectStore"
27
- }, "Platform", undefined, undefined, undefined, undefined)
28
- }, opts !== undefined ? Primitive_option.valFromOption(opts) : undefined);
41
+ }, match[1], undefined, undefined, plugin, undefined)
42
+ }, newrecord);
29
43
  new (Aws.s3.BucketPublicAccessBlock)(name + "-pab", {
30
44
  bucket: bucket.id,
31
45
  blockPublicAcls: true,
@@ -37,12 +51,24 @@ function make(name, corsRulesOpt, opts) {
37
51
  bucketName: bucket.bucket,
38
52
  bucketId: bucket.id,
39
53
  bucketArn: bucket.arn,
40
- bucketRegionalDomainName: bucket.bucketRegionalDomainName
54
+ bucketRegionalDomainName: bucket.bucketRegionalDomainName,
55
+ keyPrefix: keyPrefix
56
+ };
57
+ }
58
+
59
+ function underPrefix(store, keyPrefix) {
60
+ return {
61
+ bucketName: store.bucketName,
62
+ bucketId: store.bucketId,
63
+ bucketArn: store.bucketArn,
64
+ bucketRegionalDomainName: store.bucketRegionalDomainName,
65
+ keyPrefix: keyPrefix
41
66
  };
42
67
  }
43
68
 
44
69
  export {
45
70
  defaultCorsRules,
46
71
  make,
72
+ underPrefix,
47
73
  }
48
74
  /* @pulumi/aws Not a pure module */
@@ -225,17 +225,26 @@ let makeUiBundleDistribution = (
225
225
  // CachingOptimized policy is safe. Prepended to `orderedCacheBehaviors` so a
226
226
  // served path is matched before the SPA-serving default behavior and never
227
227
  // routed to the bundle bucket.
228
- let servedOriginId = (prefix: string): string => "served-" ++ prefix
229
- let servedCacheBehavior = (prefix: string): PulumiAws.CloudFront.Distribution.orderedCacheBehavior => {
228
+ let servedOriginId = (id: string): string => "served-" ++ id
229
+ let servedCacheBehavior = (
230
+ ~prefix: string,
231
+ ~originId: string,
232
+ ): PulumiAws.CloudFront.Distribution.orderedCacheBehavior => {
230
233
  pathPattern: Pulumi.Input.make(prefix ++ "/*"),
231
- targetOriginId: Pulumi.Input.make(servedOriginId(prefix)),
234
+ targetOriginId: Pulumi.Input.make(originId),
232
235
  viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
233
236
  allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
234
237
  cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
235
238
  cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
236
239
  }
237
240
  let orderedCacheBehaviors = Array.concat(
238
- servedBuckets->Array.map(sb => servedCacheBehavior(sb.prefix)),
241
+ // One behavior per served prefix; several prefixes may share one origin when
242
+ // they live in the same bucket.
243
+ servedBuckets->Array.flatMap(sb =>
244
+ sb.prefixes->Array.map(prefix =>
245
+ servedCacheBehavior(~prefix, ~originId=servedOriginId(sb.id))
246
+ )
247
+ ),
239
248
  Array.concat(
240
249
  [noCacheBehavior("/remoteEntry.js")],
241
250
  spaFallback ? [noCacheBehavior("/" ++ indexDocument), noCacheBehavior("/config.json")] : [],
@@ -336,7 +345,7 @@ let makeUiBundleDistribution = (
336
345
  // per-bucket read grant is the served bucket's own BucketPolicy below).
337
346
  servedBuckets->Array.map(sb => {
338
347
  PulumiAws.CloudFront.Distribution.domainName: sb.bucketRegionalDomainName,
339
- originId: Pulumi.Input.make(servedOriginId(sb.prefix)),
348
+ originId: Pulumi.Input.make(servedOriginId(sb.id)),
340
349
  originAccessControlId: Pulumi.Input.make(oacId),
341
350
  }),
342
351
  )
@@ -432,9 +441,13 @@ let makeUiBundleDistribution = (
432
441
  // object is fetchable at `https://<ui-domain>/{prefix}/<key>` while the bucket
433
442
  // stays private (direct S3 GET is 403). Mirrors the bundle bucket policy above;
434
443
  // the served bucket keeps its own all-true BucketPublicAccessBlock (app-owned).
444
+ //
445
+ // Exactly one policy per bucket — S3 permits no more. Grouping prefixes into
446
+ // one `servedBucket` is what makes that structural rather than a rule to
447
+ // remember.
435
448
  servedBuckets->Array.forEach(sb => {
436
449
  let _ = PulumiAws.S3.BucketPolicy.make(
437
- ~name=name ++ "-served-" ++ sb.prefix ++ "-policy",
450
+ ~name=name ++ "-served-" ++ sb.id ++ "-policy",
438
451
  ~args={
439
452
  bucket: sb.bucketId,
440
453
  policy: (sb.bucketArn->Pulumi.Output.fromInput, distribution.arn)
@@ -525,3 +538,193 @@ let makeUiBundleDistribution = (
525
538
  bucketName: bucket.bucket,
526
539
  }
527
540
  }
541
+
542
+ /**
543
+ Front a set of served buckets with a CloudFront distribution of their own, with
544
+ no SPA bundle behind it.
545
+
546
+ `makeUiBundleDistribution` can already serve buckets, but only as a side car to
547
+ a UI bundle. A platform that deploys no shell — because its UI ships from its
548
+ own stack — still provisions stores and then has nothing to serve them from:
549
+ the bucket is created with an all-true public-access block and takes its read
550
+ grant solely from a distribution's `BucketPolicy`, so with no distribution the
551
+ objects are unreachable by anything.
552
+
553
+ Serving lives with the stack that owns the buckets, and that is not a style
554
+ preference. **S3 permits exactly one bucket policy per bucket.** If the read
555
+ grant were written by whichever stack happened to build a distribution, two
556
+ distributions fronting one store would silently overwrite each other's grant —
557
+ deploying green and 404ing the loser's objects. Keeping the policy next to the
558
+ bucket makes a second writer impossible rather than merely discouraged.
559
+
560
+ Returns the public base URL. A consumer needs that and the store's prefix to
561
+ address an object; it needs no bucket identity at all, which is both a smaller
562
+ contract and one that cannot collide.
563
+ */
564
+ let makeServedBucketDistribution = (
565
+ ~name: string,
566
+ ~servedBuckets: array<ReventlessInfra.Platform.servedBucket>,
567
+ ~customDomain: option<customDomain>=?,
568
+ ): Pulumi.Output.t<string> => {
569
+ let oac = PulumiAws.CloudFront.OriginAccessControl.make(
570
+ ~name=name ++ "-oac",
571
+ ~args={
572
+ originAccessControlOriginType: Pulumi.Input.make("s3"),
573
+ signingBehavior: Pulumi.Input.make("always"),
574
+ signingProtocol: Pulumi.Input.make("sigv4"),
575
+ },
576
+ )
577
+
578
+ let originIdFor = (id: string): string => "served-" ++ id
579
+
580
+ // CloudFront requires a default behavior, and with no bundle origin there is
581
+ // nothing neutral to point it at. The first served bucket takes it; every
582
+ // real path is matched by its own `{prefix}/*` behavior first, so the default
583
+ // is only reached by a request for a path no store claims — which 404s at S3,
584
+ // the correct answer.
585
+ let defaultOriginId = switch servedBuckets->Array.get(0) {
586
+ | Some(sb) => originIdFor(sb.id)
587
+ | None =>
588
+ JsError.throwWithMessage(
589
+ "Plugin_Stack.makeServedBucketDistribution: no served buckets — call only when at least one store is declared",
590
+ )
591
+ }
592
+
593
+ let viewerCertificate: PulumiAws.CloudFront.Distribution.viewerCertificate = switch customDomain {
594
+ | None => {
595
+ cloudfrontDefaultCertificate: Pulumi.Input.make(true),
596
+ }
597
+ | Some({fqdn, hostedZoneId: _}) =>
598
+ let usEast1 = _getUsEast1Provider()
599
+ let cert = PulumiAws.Acm.Certificate.make(
600
+ ~name=name ++ "-cert",
601
+ ~args={
602
+ domainName: Pulumi.Input.make(fqdn),
603
+ validationMethod: Pulumi.Input.make("DNS"),
604
+ },
605
+ ~opts={provider: usEast1},
606
+ )
607
+ {
608
+ acmCertificateArn: cert.arn->Pulumi.Output.asInput,
609
+ sslSupportMethod: Pulumi.Input.make("sni-only"),
610
+ minimumProtocolVersion: Pulumi.Input.make("TLSv1.2_2021"),
611
+ }
612
+ }
613
+
614
+ let distribution = PulumiAws.CloudFront.Distribution.make(
615
+ ~name=name ++ "-cdn",
616
+ ~args={
617
+ enabled: Pulumi.Input.make(true),
618
+ aliases: ?switch customDomain {
619
+ | None => None
620
+ | Some({fqdn}) => Some(Pulumi.Input.make([fqdn]))
621
+ },
622
+ origins: oac.id->Pulumi.Output.apply(oacId =>
623
+ servedBuckets->Array.map(sb => {
624
+ PulumiAws.CloudFront.Distribution.domainName: sb.bucketRegionalDomainName,
625
+ originId: Pulumi.Input.make(originIdFor(sb.id)),
626
+ originAccessControlId: Pulumi.Input.make(oacId),
627
+ })
628
+ )
629
+ ->Pulumi.Output.asInput,
630
+ defaultCacheBehavior: Pulumi.Input.make(
631
+ (
632
+ {
633
+ targetOriginId: Pulumi.Input.make(defaultOriginId),
634
+ viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
635
+ allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
636
+ cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
637
+ cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
638
+ }: PulumiAws.CloudFront.Distribution.defaultCacheBehavior
639
+ ),
640
+ ),
641
+ // One behavior per served prefix. Store objects have immutable uuid keys,
642
+ // so the long-TTL CachingOptimized policy is safe.
643
+ orderedCacheBehaviors: Pulumi.Input.make(
644
+ servedBuckets->Array.flatMap(sb =>
645
+ sb.prefixes->Array.map(prefix => {
646
+ PulumiAws.CloudFront.Distribution.pathPattern: Pulumi.Input.make(prefix ++ "/*"),
647
+ targetOriginId: Pulumi.Input.make(originIdFor(sb.id)),
648
+ viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
649
+ allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
650
+ cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
651
+ cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
652
+ })
653
+ ),
654
+ ),
655
+ restrictions: Pulumi.Input.make({
656
+ PulumiAws.CloudFront.Distribution.geoRestriction: Pulumi.Input.make({
657
+ PulumiAws.CloudFront.Distribution.restrictionType: Pulumi.Input.make("none"),
658
+ }),
659
+ }),
660
+ viewerCertificate: Pulumi.Input.make(viewerCertificate),
661
+ comment: Pulumi.Input.make(name ++ " object store CDN"),
662
+ tags: AWS.Tags.make(
663
+ ~name=name ++ "-cdn",
664
+ ~kind=ReventlessCore.ComponentType.Platform,
665
+ ~role=Hosting,
666
+ ~scope=Platform,
667
+ ),
668
+ },
669
+ )
670
+
671
+ switch customDomain {
672
+ | None => ()
673
+ | Some({fqdn, hostedZoneId}) =>
674
+ let _ = PulumiAws.Route53.Record.make(
675
+ ~name=name ++ "-domain-alias",
676
+ ~args={
677
+ zoneId: Pulumi.Input.make(hostedZoneId),
678
+ name: Pulumi.Input.make(fqdn),
679
+ type_: Pulumi.Input.make("A"),
680
+ aliases: [
681
+ (
682
+ {
683
+ name: distribution.domainName->Pulumi.Output.asInput,
684
+ zoneId: Pulumi.Input.make(cloudFrontAliasZoneId),
685
+ evaluateTargetHealth: Pulumi.Input.make(false),
686
+ }: PulumiAws.Route53.Record.alias
687
+ ),
688
+ ]->Pulumi.Input.make,
689
+ },
690
+ )
691
+ }
692
+
693
+ // One policy per bucket — see the module doc above for why this is here and
694
+ // not in the consuming stack.
695
+ servedBuckets->Array.forEach(sb => {
696
+ let _ = PulumiAws.S3.BucketPolicy.make(
697
+ ~name=name ++ "-served-" ++ sb.id ++ "-policy",
698
+ ~args={
699
+ bucket: sb.bucketId,
700
+ policy: (sb.bucketArn->Pulumi.Output.fromInput, distribution.arn)
701
+ ->Pulumi.Output.all2
702
+ ->Pulumi.Output.apply(((bucketArn, distributionArn)) =>
703
+ {
704
+ "Version": "2012-10-17",
705
+ "Statement": [
706
+ {
707
+ "Sid": "AllowCloudFrontServicePrincipal",
708
+ "Effect": "Allow",
709
+ "Principal": {"Service": "cloudfront.amazonaws.com"},
710
+ "Action": "s3:GetObject",
711
+ "Resource": bucketArn ++ "/*",
712
+ "Condition": {
713
+ "StringEquals": {"AWS:SourceArn": distributionArn},
714
+ },
715
+ },
716
+ ],
717
+ }
718
+ ->JSON.stringifyAny
719
+ ->Option.getUnsafe
720
+ )
721
+ ->Pulumi.Output.asInput,
722
+ },
723
+ )
724
+ })
725
+
726
+ switch customDomain {
727
+ | Some({fqdn}) => Pulumi.Output.make("https://" ++ fqdn)
728
+ | None => distribution.domainName->Pulumi.Output.apply(d => "https://" ++ d)
729
+ }
730
+ }
@@ -119,11 +119,11 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
119
119
  ],
120
120
  cachePolicyId: cachingDisabledPolicyId
121
121
  });
122
- let orderedCacheBehaviors = servedBuckets.map(sb => {
123
- let prefix = sb.prefix;
122
+ let orderedCacheBehaviors = servedBuckets.flatMap(sb => sb.prefixes.map(prefix => {
123
+ let originId = "served-" + sb.id;
124
124
  return {
125
125
  pathPattern: prefix + "/*",
126
- targetOriginId: "served-" + prefix,
126
+ targetOriginId: originId,
127
127
  viewerProtocolPolicy: "redirect-to-https",
128
128
  allowedMethods: [
129
129
  "GET",
@@ -135,7 +135,7 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
135
135
  ],
136
136
  cachePolicyId: cachingOptimizedPolicyId
137
137
  };
138
- }).concat([noCacheBehavior("/remoteEntry.js")].concat(spaFallback ? [
138
+ })).concat([noCacheBehavior("/remoteEntry.js")].concat(spaFallback ? [
139
139
  noCacheBehavior("/" + indexDocument),
140
140
  noCacheBehavior("/config.json")
141
141
  ] : []));
@@ -188,7 +188,7 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
188
188
  originAccessControlId: oacId
189
189
  }].concat(servedBuckets.map(sb => ({
190
190
  domainName: sb.bucketRegionalDomainName,
191
- originId: "served-" + sb.prefix,
191
+ originId: "served-" + sb.id,
192
192
  originAccessControlId: oacId
193
193
  })));
194
194
  }),
@@ -253,7 +253,7 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
253
253
  }))
254
254
  });
255
255
  servedBuckets.forEach(sb => {
256
- new (Aws.s3.BucketPolicy)(name + "-served-" + sb.prefix + "-policy", {
256
+ new (Aws.s3.BucketPolicy)(name + "-served-" + sb.id + "-policy", {
257
257
  bucket: sb.bucketId,
258
258
  policy: Pulumi.all([
259
259
  sb.bucketArn,
@@ -303,6 +303,121 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
303
303
  };
304
304
  }
305
305
 
306
+ function makeServedBucketDistribution(name, servedBuckets, customDomain) {
307
+ let oac = new (Aws.cloudfront.OriginAccessControl)(name + "-oac", {
308
+ originAccessControlOriginType: "s3",
309
+ signingBehavior: "always",
310
+ signingProtocol: "sigv4"
311
+ });
312
+ let sb = servedBuckets[0];
313
+ let defaultOriginId = sb !== undefined ? "served-" + sb.id : Stdlib_JsError.throwWithMessage("Plugin_Stack.makeServedBucketDistribution: no served buckets — call only when at least one store is declared");
314
+ let viewerCertificate;
315
+ if (customDomain !== undefined) {
316
+ let usEast1 = _getUsEast1Provider();
317
+ let cert = new (Aws.acm.Certificate)(name + "-cert", {
318
+ domainName: customDomain.fqdn,
319
+ validationMethod: "DNS"
320
+ }, {
321
+ provider: Primitive_option.some(usEast1)
322
+ });
323
+ viewerCertificate = {
324
+ acmCertificateArn: cert.arn,
325
+ sslSupportMethod: "sni-only",
326
+ minimumProtocolVersion: "TLSv1.2_2021"
327
+ };
328
+ } else {
329
+ viewerCertificate = {
330
+ cloudfrontDefaultCertificate: true
331
+ };
332
+ }
333
+ let distribution = new (Aws.cloudfront.Distribution)(name + "-cdn", {
334
+ enabled: true,
335
+ aliases: customDomain !== undefined ? [customDomain.fqdn] : undefined,
336
+ origins: oac.id.apply(oacId => servedBuckets.map(sb => ({
337
+ domainName: sb.bucketRegionalDomainName,
338
+ originId: "served-" + sb.id,
339
+ originAccessControlId: oacId
340
+ }))),
341
+ defaultCacheBehavior: {
342
+ targetOriginId: defaultOriginId,
343
+ viewerProtocolPolicy: "redirect-to-https",
344
+ allowedMethods: [
345
+ "GET",
346
+ "HEAD"
347
+ ],
348
+ cachedMethods: [
349
+ "GET",
350
+ "HEAD"
351
+ ],
352
+ cachePolicyId: cachingOptimizedPolicyId
353
+ },
354
+ orderedCacheBehaviors: servedBuckets.flatMap(sb => sb.prefixes.map(prefix => ({
355
+ pathPattern: prefix + "/*",
356
+ targetOriginId: "served-" + sb.id,
357
+ viewerProtocolPolicy: "redirect-to-https",
358
+ allowedMethods: [
359
+ "GET",
360
+ "HEAD"
361
+ ],
362
+ cachedMethods: [
363
+ "GET",
364
+ "HEAD"
365
+ ],
366
+ cachePolicyId: cachingOptimizedPolicyId
367
+ }))),
368
+ restrictions: {
369
+ geoRestriction: {
370
+ restrictionType: "none"
371
+ }
372
+ },
373
+ viewerCertificate: viewerCertificate,
374
+ comment: name + " object store CDN",
375
+ tags: AWS_Tags$ReventlessAws.make(name + "-cdn", "Platform", "Hosting", "Platform", undefined, undefined, undefined, undefined)
376
+ });
377
+ if (customDomain !== undefined) {
378
+ new (Aws.route53.Record)(name + "-domain-alias", {
379
+ zoneId: customDomain.hostedZoneId,
380
+ name: customDomain.fqdn,
381
+ type: "A",
382
+ aliases: [{
383
+ name: distribution.domainName,
384
+ zoneId: cloudFrontAliasZoneId,
385
+ evaluateTargetHealth: false
386
+ }]
387
+ });
388
+ }
389
+ servedBuckets.forEach(sb => {
390
+ new (Aws.s3.BucketPolicy)(name + "-served-" + sb.id + "-policy", {
391
+ bucket: sb.bucketId,
392
+ policy: Pulumi.all([
393
+ sb.bucketArn,
394
+ distribution.arn
395
+ ]).apply(param => JSON.stringify({
396
+ Version: "2012-10-17",
397
+ Statement: [{
398
+ Sid: "AllowCloudFrontServicePrincipal",
399
+ Effect: "Allow",
400
+ Principal: {
401
+ Service: "cloudfront.amazonaws.com"
402
+ },
403
+ Action: "s3:GetObject",
404
+ Resource: param[0] + "/*",
405
+ Condition: {
406
+ StringEquals: {
407
+ "AWS:SourceArn": param[1]
408
+ }
409
+ }
410
+ }]
411
+ }))
412
+ });
413
+ });
414
+ if (customDomain !== undefined) {
415
+ return Pulumi.output("https://" + customDomain.fqdn);
416
+ } else {
417
+ return distribution.domainName.apply(d => "https://" + d);
418
+ }
419
+ }
420
+
306
421
  export {
307
422
  log,
308
423
  cachingOptimizedPolicyId,
@@ -313,5 +428,6 @@ export {
313
428
  cacheControlFor,
314
429
  invalidateDistribution,
315
430
  makeUiBundleDistribution,
431
+ makeServedBucketDistribution,
316
432
  }
317
433
  /* log Not a pure module */
@@ -26,3 +26,20 @@ let parseProdStacks = (csv: string): array<string> =>
26
26
  ->String.split(",")
27
27
  ->Array.map(String.trim)
28
28
  ->Array.filter(s => s !== "")
29
+
30
+ /**
31
+ The stacks this deployment considers production, resolved from config.
32
+
33
+ The one notion of "is this stack production", deliberately shared rather than
34
+ answered twice. Domain naming asked it first; object-store layout asks it too,
35
+ and a second key would be two flags that can disagree — the state where a stack
36
+ gets a production domain and non-production storage, or the reverse, with
37
+ nothing to flag the mismatch.
38
+
39
+ Impure by necessity (it reads config); the derivations that consume it stay
40
+ pure and unit-tested.
41
+ */
42
+ let resolveProdStacks = (): array<string> =>
43
+ Util_LocalConfig.get("hostUiProdStacks")
44
+ ->Option.map(parseProdStacks)
45
+ ->Option.getOr(defaultProdStacks)
@@ -1,5 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Util_LocalConfig$ReventlessAws from "./Util_LocalConfig.res.mjs";
3
5
 
4
6
  let defaultProdStacks = [
5
7
  "prod",
@@ -18,9 +20,14 @@ function parseProdStacks(csv) {
18
20
  return csv.split(",").map(prim => prim.trim()).filter(s => s !== "");
19
21
  }
20
22
 
23
+ function resolveProdStacks() {
24
+ return Stdlib_Option.getOr(Stdlib_Option.map(Util_LocalConfig$ReventlessAws.get("hostUiProdStacks"), parseProdStacks), defaultProdStacks);
25
+ }
26
+
21
27
  export {
22
28
  defaultProdStacks,
23
29
  deriveFqdn,
24
30
  parseProdStacks,
31
+ resolveProdStacks,
25
32
  }
26
- /* No side effect */
33
+ /* Util_LocalConfig-ReventlessAws Not a pure module */