@openhi/constructs 0.0.128 → 0.0.130

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/lib/index.mjs CHANGED
@@ -1526,6 +1526,8 @@ import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
1526
1526
  import { Bucket as Bucket2 } from "aws-cdk-lib/aws-s3";
1527
1527
  import { Construct as Construct8 } from "constructs";
1528
1528
  var STATIC_HOSTING_SERVICE_TYPE = "website";
1529
+ var PER_BRANCH_PREVIEW_PREFIX = "admin-pr-";
1530
+ var DEFAULT_PREVIEW_EXPIRATION_DAYS = 14;
1529
1531
  var _StaticHosting = class _StaticHosting extends Construct8 {
1530
1532
  /**
1531
1533
  * Returns true when `domainName` begins with a wildcard label (`*.`),
@@ -1535,11 +1537,19 @@ var _StaticHosting = class _StaticHosting extends Construct8 {
1535
1537
  static isWildcardDomain(domainName) {
1536
1538
  return domainName.startsWith("*.");
1537
1539
  }
1538
- constructor(scope, id, props = {}) {
1540
+ constructor(scope, id, props) {
1539
1541
  super(scope, id);
1540
1542
  const stack = OpenHiService.of(scope);
1541
1543
  const serviceType = props.serviceType ?? STATIC_HOSTING_SERVICE_TYPE;
1542
1544
  const hostingMode = props.hostingMode ?? "spa";
1545
+ const previewLifecycleRules = props.enablePreviewLifecycle ? [
1546
+ {
1547
+ id: "expire-pr-previews",
1548
+ enabled: true,
1549
+ prefix: props.prefixPattern,
1550
+ expiration: props.previewExpiration ?? Duration5.days(DEFAULT_PREVIEW_EXPIRATION_DAYS)
1551
+ }
1552
+ ] : void 0;
1543
1553
  this.bucket = new Bucket2(this, "bucket", {
1544
1554
  blockPublicAccess: {
1545
1555
  blockPublicAcls: true,
@@ -1547,6 +1557,9 @@ var _StaticHosting = class _StaticHosting extends Construct8 {
1547
1557
  ignorePublicAcls: true,
1548
1558
  restrictPublicBuckets: true
1549
1559
  },
1560
+ ...previewLifecycleRules !== void 0 && {
1561
+ lifecycleRules: previewLifecycleRules
1562
+ },
1550
1563
  ...props.bucketProps
1551
1564
  });
1552
1565
  const handlerJs = path6.join(
@@ -2509,6 +2522,26 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
2509
2522
  });
2510
2523
  return UserPoolDomain2.fromDomainName(scope, "user-pool-domain", domainName);
2511
2524
  }
2525
+ /**
2526
+ * Returns the full Cognito Hosted UI base URL (e.g.
2527
+ * `https://auth-abc.auth.us-east-2.amazoncognito.com`) by looking up
2528
+ * the Auth stack's User Pool Domain from SSM and composing it with the
2529
+ * calling stack's region.
2530
+ *
2531
+ * Equivalent to `UserPoolDomain.baseUrl()` on the concrete construct,
2532
+ * but works across stacks where the looked-up `IUserPoolDomain` is an
2533
+ * `Import` and does not carry the `baseUrl()` method. Assumes the
2534
+ * domain was created as a Cognito-managed prefix domain (the only
2535
+ * variant `OpenHiAuthService.createUserPoolDomain` produces).
2536
+ */
2537
+ static userPoolDomainBaseUrlFromConstruct(scope) {
2538
+ const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
2539
+ ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
2540
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
2541
+ });
2542
+ const region = Stack7.of(scope).region;
2543
+ return `https://${domainName}.auth.${region}.amazoncognito.com`;
2544
+ }
2512
2545
  /**
2513
2546
  * Returns an IKey (KMS) by looking up the Auth stack's User Pool KMS Key ARN from SSM.
2514
2547
  */
@@ -3118,11 +3151,11 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3118
3151
  const cognitoScope = new Construct21(this, "runtime-config");
3119
3152
  const userPool = OpenHiAuthService.userPoolFromConstruct(cognitoScope);
3120
3153
  const userPoolClient = OpenHiAuthService.userPoolClientFromConstruct(cognitoScope);
3121
- const userPoolDomain = OpenHiAuthService.userPoolDomainFromConstruct(cognitoScope);
3154
+ const cognitoDomainUrl = OpenHiAuthService.userPoolDomainBaseUrlFromConstruct(cognitoScope);
3122
3155
  return {
3123
3156
  OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_ID: userPool.userPoolId,
3124
3157
  OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_CLIENT_ID: userPoolClient.userPoolClientId,
3125
- OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN: userPoolDomain.domainName,
3158
+ OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN_URL: cognitoDomainUrl,
3126
3159
  OPENHI_RUNTIME_CONFIG_COGNITO_REDIRECT_URI: this.props.runtimeConfig.cognitoRedirectUri,
3127
3160
  OPENHI_RUNTIME_CONFIG_API_BASE_URL: this.props.runtimeConfig.apiBaseUrl
3128
3161
  };
@@ -3172,7 +3205,9 @@ _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
3172
3205
  var OpenHiGraphqlService = _OpenHiGraphqlService;
3173
3206
 
3174
3207
  // src/services/open-hi-website-service.ts
3208
+ var import_config5 = __toESM(require_lib2());
3175
3209
  import { Bucket as Bucket3 } from "aws-cdk-lib/aws-s3";
3210
+ var OPENHI_PR_NUMBER_ENV_VAR = "OPENHI_PR_NUMBER";
3176
3211
  var SSM_PARAM_NAME_FULL_DOMAIN = "WEBSITE_FULL_DOMAIN";
3177
3212
  var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3178
3213
  /**
@@ -3232,9 +3267,16 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3232
3267
  super(ohEnv, _OpenHiWebsiteService.SERVICE_TYPE, props);
3233
3268
  this.props = props;
3234
3269
  this.validateConfig(props);
3270
+ const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
3271
+ this.prNumber = this.resolvePrNumber(props);
3272
+ if (!isReleaseBranch && this.prNumber === void 0) {
3273
+ throw new Error(
3274
+ `OpenHiWebsiteService: prNumber is required on non-release-branch deploys (branchName="${this.branchName}", defaultReleaseBranch="${this.defaultReleaseBranch}"). Pass the \`prNumber\` prop or set the ${OPENHI_PR_NUMBER_ENV_VAR} env var.`
3275
+ );
3276
+ }
3235
3277
  const hostedZone = this.createHostedZone();
3236
3278
  this.fullDomain = this.computeFullDomain(hostedZone);
3237
- const shouldCreateHostingInfra = props.createHostingInfrastructure ?? this.branchName === this.defaultReleaseBranch;
3279
+ const shouldCreateHostingInfra = props.createHostingInfrastructure ?? isReleaseBranch;
3238
3280
  if (shouldCreateHostingInfra) {
3239
3281
  const certificate = this.createCertificate();
3240
3282
  this.staticHosting = this.createStaticHosting({
@@ -3242,6 +3284,8 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3242
3284
  hostedZone
3243
3285
  });
3244
3286
  this.createFullDomainParameter();
3287
+ } else if (!isReleaseBranch) {
3288
+ this.perBranchHostname = this.createPerBranchHostname(hostedZone);
3245
3289
  }
3246
3290
  if (props.createStaticContent !== false) {
3247
3291
  const bucket = this.resolveStaticHostingBucket();
@@ -3284,25 +3328,76 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3284
3328
  return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
3285
3329
  }
3286
3330
  /**
3287
- * Computes the full website domain from `domainPrefix` and the child
3288
- * zone name.
3331
+ * Resolves the PR number from props or the `OPENHI_PR_NUMBER` env var.
3332
+ * Returns `undefined` on release-branch deploys where no PR number is
3333
+ * needed.
3334
+ */
3335
+ resolvePrNumber(props) {
3336
+ if (props.prNumber !== void 0) {
3337
+ return props.prNumber;
3338
+ }
3339
+ const raw = process.env[OPENHI_PR_NUMBER_ENV_VAR]?.trim();
3340
+ if (!raw) {
3341
+ return void 0;
3342
+ }
3343
+ const parsed = Number.parseInt(raw, 10);
3344
+ if (!Number.isInteger(parsed) || parsed <= 0) {
3345
+ throw new Error(
3346
+ `${OPENHI_PR_NUMBER_ENV_VAR} must be a positive integer; got "${raw}".`
3347
+ );
3348
+ }
3349
+ return parsed;
3350
+ }
3351
+ /**
3352
+ * Computes the full website domain from `domainPrefix`, the PR number,
3353
+ * and the child zone name. Release-branch deploys serve at
3354
+ * `\<domainPrefix\>.\<zone\>` (e.g. `admin.dev.openhi.org`); every other
3355
+ * deploy serves a per-PR preview at `\<domainPrefix\>-pr-\<N\>.\<zone\>`
3356
+ * (e.g. `admin-pr-123.dev.openhi.org`).
3289
3357
  */
3290
3358
  computeFullDomain(hostedZone) {
3291
- const prefix = this.props.domainPrefix ?? "www";
3292
- return [prefix, hostedZone.zoneName].join(".");
3359
+ const subDomain = this.computeSubDomain();
3360
+ return [subDomain, hostedZone.zoneName].join(".");
3361
+ }
3362
+ /**
3363
+ * Returns the sub-domain label (left of the zone) for the current
3364
+ * deploy. Used both for {@link fullDomain} and for the per-branch S3
3365
+ * key prefix passed to {@link StaticContent} so the upload prefix
3366
+ * always matches the served hostname.
3367
+ *
3368
+ * Non-release deploys compose the per-PR slug from
3369
+ * {@link PER_BRANCH_PREVIEW_PREFIX} so the per-PR S3 key prefix
3370
+ * matches what `StaticHosting`'s lifecycle rule expires.
3371
+ */
3372
+ computeSubDomain() {
3373
+ const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
3374
+ if (isReleaseBranch) {
3375
+ return this.props.domainPrefix ?? "www";
3376
+ }
3377
+ return `${PER_BRANCH_PREVIEW_PREFIX}${this.prNumber}`;
3293
3378
  }
3294
3379
  /**
3295
3380
  * Creates the StaticHosting infrastructure (bucket + distribution +
3296
- * Lambda@Edge + 4 SSM params + DNS).
3381
+ * Lambda@Edge + 4 SSM params + DNS). The release-branch distribution
3382
+ * adds `*.\<zone\>` as a wildcard alt-name on top of the canonical
3383
+ * hostname so per-PR previews resolve via the same distribution.
3384
+ *
3385
+ * The bucket carries an S3 lifecycle rule that expires per-PR
3386
+ * preview content (keys under {@link PER_BRANCH_PREVIEW_PREFIX})
3387
+ * on non-production stages. PROD never gets the rule — see
3388
+ * `enablePreviewLifecycle`.
3297
3389
  */
3298
3390
  createStaticHosting(deps) {
3299
3391
  const restApi = this.props.restApi === true ? this.resolveRestApi() : void 0;
3392
+ const wildcardSan = `*.${deps.hostedZone.zoneName}`;
3300
3393
  return new StaticHosting(this, "static-hosting", {
3301
3394
  serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
3302
3395
  certificate: deps.certificate,
3303
3396
  hostedZone: deps.hostedZone,
3304
- domainNames: [this.fullDomain],
3397
+ domainNames: [this.fullDomain, wildcardSan],
3305
3398
  description: `OpenHI website (${this.fullDomain})`,
3399
+ prefixPattern: PER_BRANCH_PREVIEW_PREFIX,
3400
+ enablePreviewLifecycle: this.ohEnv.ohStage.stageType !== import_config5.OPEN_HI_STAGE.PROD,
3306
3401
  ...restApi !== void 0 && { restApi }
3307
3402
  });
3308
3403
  }
@@ -3336,6 +3431,12 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3336
3431
  * the release-branch deploy publishes to SSM, addressed against
3337
3432
  * {@link OpenHiService.releaseBranchHash}. See
3338
3433
  * {@link resolveStaticHostingBucket}.
3434
+ *
3435
+ * The S3 key prefix is `\<sub-domain\>.\<zone\>/\<contentDest\>` so the
3436
+ * upload location matches the Host-header-derived folder the Lambda@Edge
3437
+ * viewer-request handler prepends. Passing the zone name (rather than
3438
+ * `this.fullDomain`) for the `fullDomain` prop keeps the prefix flat
3439
+ * — `admin-pr-123.dev.openhi.org/`, not `admin-pr-123.admin.dev.openhi.org/`.
3339
3440
  */
3340
3441
  createStaticContent(bucket) {
3341
3442
  const { contentSourceDirectory, contentDestinationDirectory } = this.props;
@@ -3343,7 +3444,21 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3343
3444
  bucket,
3344
3445
  contentSourceDirectory,
3345
3446
  contentDestinationDirectory,
3346
- fullDomain: this.fullDomain
3447
+ subDomain: this.computeSubDomain(),
3448
+ fullDomain: this.config.zoneName
3449
+ });
3450
+ }
3451
+ /**
3452
+ * Creates the per-PR `PerBranchHostname` alias record on non-release
3453
+ * branch deploys. The record points `\<domainPrefix\>-pr-\<N\>.\<zone\>`
3454
+ * at the release-branch CloudFront distribution (resolved from SSM
3455
+ * against {@link OpenHiService.releaseBranchHash}).
3456
+ */
3457
+ createPerBranchHostname(hostedZone) {
3458
+ return new PerBranchHostname(this, "per-branch-hostname", {
3459
+ hostname: this.fullDomain,
3460
+ hostedZone,
3461
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3347
3462
  });
3348
3463
  }
3349
3464
  /**
@@ -3902,6 +4017,7 @@ export {
3902
4017
  DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
3903
4018
  DATA_STORE_CHANGE_DETAIL_TYPE,
3904
4019
  DATA_STORE_CHANGE_EVENT_SOURCE,
4020
+ DEFAULT_PREVIEW_EXPIRATION_DAYS,
3905
4021
  DEMO_DATA_PLANE_FIXTURES,
3906
4022
  DEMO_PERIOD,
3907
4023
  DEMO_TENANT_SPECS,
@@ -3912,6 +4028,7 @@ export {
3912
4028
  DataStorePostgresReplica,
3913
4029
  DiscoverableStringParameter,
3914
4030
  DynamoDbDataStore,
4031
+ OPENHI_PR_NUMBER_ENV_VAR,
3915
4032
  OPENHI_REPO_TAG_KEY_ENV_VAR,
3916
4033
  OPENHI_RESOURCE_URN_SYSTEM,
3917
4034
  OPENHI_TAG_KEY_PREFIX_ENV_VAR,
@@ -3937,6 +4054,7 @@ export {
3937
4054
  OpsEventBus,
3938
4055
  OwningDeleteCascadeLambdas,
3939
4056
  OwningDeleteCascadeWorkflow,
4057
+ PER_BRANCH_PREVIEW_PREFIX,
3940
4058
  PLACEHOLDER_TENANT_ID,
3941
4059
  PLACEHOLDER_WORKSPACE_ID,
3942
4060
  PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM,