@openhi/constructs 0.0.133 → 0.0.135

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.js CHANGED
@@ -796,7 +796,6 @@ __export(src_exports, {
796
796
  DataStorePostgresReplica: () => DataStorePostgresReplica,
797
797
  DiscoverableStringParameter: () => DiscoverableStringParameter,
798
798
  DynamoDbDataStore: () => DynamoDbDataStore,
799
- OPENHI_PR_NUMBER_ENV_VAR: () => OPENHI_PR_NUMBER_ENV_VAR,
800
799
  OPENHI_REPO_TAG_KEY_ENV_VAR: () => OPENHI_REPO_TAG_KEY_ENV_VAR,
801
800
  OPENHI_RESOURCE_URN_SYSTEM: () => OPENHI_RESOURCE_URN_SYSTEM,
802
801
  OPENHI_TAG_KEY_PREFIX_ENV_VAR: () => OPENHI_TAG_KEY_PREFIX_ENV_VAR,
@@ -1207,10 +1206,14 @@ var OpenHiService = class extends import_aws_cdk_lib4.Stack {
1207
1206
  );
1208
1207
  }
1209
1208
  /**
1210
- * DNS prefix for this branche's child zone.
1209
+ * DNS prefix for this branche's child zone. Capped at 56 chars so
1210
+ * that a `<service>-<prefix>` hostname segment stays under the 63-byte
1211
+ * DNS label limit even for the longest current prefix (`admin-`, 6
1212
+ * bytes; the matching API uses `api-`, 4 bytes). 56 leaves 1 byte of
1213
+ * headroom on the longer side.
1211
1214
  */
1212
1215
  get childZonePrefix() {
1213
- return (0, import_change_case.paramCase)(this.branchName).slice(0, 200);
1216
+ return (0, import_change_case.paramCase)(this.branchName).slice(0, 56);
1214
1217
  }
1215
1218
  };
1216
1219
 
@@ -2355,7 +2358,7 @@ var import_aws_route53_targets = require("aws-cdk-lib/aws-route53-targets");
2355
2358
  var import_aws_s3 = require("aws-cdk-lib/aws-s3");
2356
2359
  var import_constructs8 = require("constructs");
2357
2360
  var STATIC_HOSTING_SERVICE_TYPE = "website";
2358
- var PER_BRANCH_PREVIEW_PREFIX = "admin-pr-";
2361
+ var PER_BRANCH_PREVIEW_PREFIX = "admin-";
2359
2362
  var DEFAULT_PREVIEW_EXPIRATION_DAYS = 14;
2360
2363
  var _StaticHosting = class _StaticHosting extends import_constructs8.Construct {
2361
2364
  /**
@@ -2434,10 +2437,14 @@ var _StaticHosting = class _StaticHosting extends import_constructs8.Construct {
2434
2437
  originAccessLevels: [import_aws_cloudfront.AccessLevel.READ]
2435
2438
  });
2436
2439
  const hasCustomDomain = props.certificate !== void 0 && props.hostedZone !== void 0 && props.domainNames !== void 0 && props.domainNames.length > 0;
2437
- const additionalBehaviors = this.buildRestApiBehaviors(
2440
+ const restApiWiring = this.buildRestApiBehaviors(
2438
2441
  stack.branchHash,
2439
2442
  props.restApi
2440
2443
  );
2444
+ const additionalBehaviors = restApiWiring?.behaviors;
2445
+ this.configJsonRewriteFunction = restApiWiring?.configJsonRewriteFunction;
2446
+ this.hostCopyFunction = restApiWiring?.hostCopyFunction;
2447
+ this.originRequestHandler = restApiWiring?.originRequestHandler;
2441
2448
  this.distribution = new import_aws_cloudfront.Distribution(this, "distribution", {
2442
2449
  comment: `Static hosting distribution for ${props.description ?? id}`,
2443
2450
  ...hasCustomDomain ? {
@@ -2501,14 +2508,20 @@ var _StaticHosting = class _StaticHosting extends import_constructs8.Construct {
2501
2508
  });
2502
2509
  }
2503
2510
  /**
2504
- * Builds the `/api/*` and `/api/control/runtime-config` behaviors backed
2505
- * by the REST API custom-domain origin. Returns `undefined` when no
2506
- * `restApi` prop is supplied so the Distribution stays S3-only.
2511
+ * Builds the `/config.json`, `/api/*`, and `/api/control/runtime-config`
2512
+ * behaviors backed by the REST API custom-domain origin, plus the
2513
+ * viewer-request CloudFront Functions and the origin-request
2514
+ * Lambda@Edge that route each request to the matching per-PR API
2515
+ * origin. Returns `undefined` when no `restApi` prop is supplied so
2516
+ * the Distribution stays S3-only.
2507
2517
  */
2508
2518
  buildRestApiBehaviors(branchHash, restApi) {
2509
2519
  if (restApi === void 0) {
2510
2520
  return void 0;
2511
2521
  }
2522
+ const runtimeConfigPath = restApi.runtimeConfigPath ?? "/control/runtime-config";
2523
+ const viewerHostPrefix = restApi.hostMapping?.viewerPrefix ?? "admin";
2524
+ const apiHostPrefix = restApi.hostMapping?.apiPrefix ?? "api";
2512
2525
  const apiOrigin = new import_aws_cloudfront_origins.HttpOrigin(restApi.domainName, {
2513
2526
  protocolPolicy: import_aws_cloudfront.OriginProtocolPolicy.HTTPS_ONLY
2514
2527
  });
@@ -2517,31 +2530,150 @@ var _StaticHosting = class _StaticHosting extends import_constructs8.Construct {
2517
2530
  "runtime-config-cache-policy",
2518
2531
  {
2519
2532
  cachePolicyName: `static-hosting-runtime-config-${branchHash}`,
2520
- comment: "/api/control/runtime-config: cache key includes only `v` so the bundle's deploy-hash bust works automatically.",
2533
+ comment: "/config.json: cache key keyed on Host + `v` so per-PR responses cannot leak across hosts.",
2521
2534
  defaultTtl: restApi.runtimeConfigCacheTtl?.defaultTtl ?? import_aws_cdk_lib11.Duration.minutes(5),
2522
2535
  minTtl: import_aws_cdk_lib11.Duration.seconds(0),
2523
2536
  maxTtl: restApi.runtimeConfigCacheTtl?.maxTtl ?? import_aws_cdk_lib11.Duration.hours(1),
2524
- headerBehavior: import_aws_cloudfront.CacheHeaderBehavior.none(),
2537
+ // `Host` keys the cache per-PR — the origin-request edge Lambda
2538
+ // forwards each PR's request to its own API origin, and two
2539
+ // PRs' /config.json payloads must not share a cache slot.
2540
+ headerBehavior: import_aws_cloudfront.CacheHeaderBehavior.allowList("Host"),
2525
2541
  queryStringBehavior: import_aws_cloudfront.CacheQueryStringBehavior.allowList("v"),
2526
2542
  cookieBehavior: import_aws_cloudfront.CacheCookieBehavior.none(),
2527
2543
  enableAcceptEncodingGzip: true,
2528
2544
  enableAcceptEncodingBrotli: true
2529
2545
  }
2530
2546
  );
2547
+ const runtimeConfigPathLiteral = JSON.stringify(runtimeConfigPath);
2548
+ const configJsonRewriteFunction = new import_aws_cloudfront.Function(
2549
+ this,
2550
+ "config-json-rewrite-function",
2551
+ {
2552
+ functionName: `static-hosting-config-json-rewrite-${branchHash}`,
2553
+ // CloudFront caps `Comment` at 128 chars; the full rationale is in
2554
+ // the preceding code comment.
2555
+ comment: "Rewrites /config.json to the runtime-config path; copies Host into x-viewer-host.",
2556
+ code: import_aws_cloudfront.FunctionCode.fromInline(
2557
+ [
2558
+ "function handler(event) {",
2559
+ " var request = event.request;",
2560
+ " if (request.headers.host && request.headers.host.value) {",
2561
+ " request.headers['x-viewer-host'] = { value: request.headers.host.value };",
2562
+ " }",
2563
+ ` request.uri = ${runtimeConfigPathLiteral};`,
2564
+ " return request;",
2565
+ "}"
2566
+ ].join("\n")
2567
+ )
2568
+ }
2569
+ );
2570
+ const hostCopyFunction = new import_aws_cloudfront.Function(
2571
+ this,
2572
+ "host-copy-function",
2573
+ {
2574
+ functionName: `static-hosting-host-copy-${branchHash}`,
2575
+ // CloudFront caps `Comment` at 128 chars; the full rationale is in
2576
+ // the preceding code comment.
2577
+ comment: "Copies viewer Host into x-viewer-host for the origin-request Lambda@Edge.",
2578
+ code: import_aws_cloudfront.FunctionCode.fromInline(
2579
+ [
2580
+ "function handler(event) {",
2581
+ " var request = event.request;",
2582
+ " if (request.headers.host && request.headers.host.value) {",
2583
+ " request.headers['x-viewer-host'] = { value: request.headers.host.value };",
2584
+ " }",
2585
+ " return request;",
2586
+ "}"
2587
+ ].join("\n")
2588
+ )
2589
+ }
2590
+ );
2591
+ const originHandlerJs = path6.join(
2592
+ __dirname,
2593
+ "static-hosting.origin-request-handler.js"
2594
+ );
2595
+ const originHandlerTs = path6.join(
2596
+ __dirname,
2597
+ "static-hosting.origin-request-handler.ts"
2598
+ );
2599
+ const originHandlerEntry = fs6.existsSync(originHandlerJs) ? originHandlerJs : originHandlerTs;
2600
+ const originRequestHandler = new import_aws_lambda_nodejs6.NodejsFunction(
2601
+ this,
2602
+ "origin-request-handler",
2603
+ {
2604
+ entry: originHandlerEntry,
2605
+ handler: "originRequestHandler",
2606
+ memorySize: 128,
2607
+ runtime: import_aws_lambda6.Runtime.NODEJS_LATEST,
2608
+ logGroup: new import_aws_logs.LogGroup(this, "origin-request-handler-log-group", {
2609
+ retention: import_aws_logs.RetentionDays.ONE_MONTH
2610
+ }),
2611
+ // Lambda@Edge forbids runtime env vars, so the host-prefix
2612
+ // mapping is inlined into the bundle at synth time via esbuild
2613
+ // `define`. The handler reads `process.env.VIEWER_HOST_PREFIX`
2614
+ // and `process.env.API_HOST_PREFIX` at module load; esbuild
2615
+ // replaces those identifiers with literal strings during
2616
+ // bundling so the shipped code carries no env-var lookups.
2617
+ bundling: {
2618
+ define: {
2619
+ "process.env.VIEWER_HOST_PREFIX": JSON.stringify(viewerHostPrefix),
2620
+ "process.env.API_HOST_PREFIX": JSON.stringify(apiHostPrefix)
2621
+ }
2622
+ }
2623
+ }
2624
+ );
2625
+ const originRequestEdgeLambda = {
2626
+ functionVersion: originRequestHandler.currentVersion,
2627
+ eventType: import_aws_cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
2628
+ includeBody: false
2629
+ };
2531
2630
  return {
2532
- "/api/control/runtime-config": {
2533
- origin: apiOrigin,
2534
- viewerProtocolPolicy: import_aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
2535
- allowedMethods: import_aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
2536
- cachePolicy: runtimeConfigCachePolicy,
2537
- originRequestPolicy: import_aws_cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER
2538
- },
2539
- "/api/*": {
2540
- origin: apiOrigin,
2541
- viewerProtocolPolicy: import_aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
2542
- allowedMethods: import_aws_cloudfront.AllowedMethods.ALLOW_ALL,
2543
- cachePolicy: import_aws_cloudfront.CachePolicy.CACHING_DISABLED,
2544
- originRequestPolicy: import_aws_cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER
2631
+ configJsonRewriteFunction,
2632
+ hostCopyFunction,
2633
+ originRequestHandler,
2634
+ behaviors: {
2635
+ "/config.json": {
2636
+ origin: apiOrigin,
2637
+ viewerProtocolPolicy: import_aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
2638
+ allowedMethods: import_aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
2639
+ cachePolicy: runtimeConfigCachePolicy,
2640
+ originRequestPolicy: import_aws_cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
2641
+ functionAssociations: [
2642
+ {
2643
+ function: configJsonRewriteFunction,
2644
+ eventType: import_aws_cloudfront.FunctionEventType.VIEWER_REQUEST
2645
+ }
2646
+ ],
2647
+ edgeLambdas: [originRequestEdgeLambda]
2648
+ },
2649
+ "/api/control/runtime-config": {
2650
+ origin: apiOrigin,
2651
+ viewerProtocolPolicy: import_aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
2652
+ allowedMethods: import_aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
2653
+ cachePolicy: runtimeConfigCachePolicy,
2654
+ originRequestPolicy: import_aws_cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
2655
+ functionAssociations: [
2656
+ {
2657
+ function: hostCopyFunction,
2658
+ eventType: import_aws_cloudfront.FunctionEventType.VIEWER_REQUEST
2659
+ }
2660
+ ],
2661
+ edgeLambdas: [originRequestEdgeLambda]
2662
+ },
2663
+ "/api/*": {
2664
+ origin: apiOrigin,
2665
+ viewerProtocolPolicy: import_aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
2666
+ allowedMethods: import_aws_cloudfront.AllowedMethods.ALLOW_ALL,
2667
+ cachePolicy: import_aws_cloudfront.CachePolicy.CACHING_DISABLED,
2668
+ originRequestPolicy: import_aws_cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
2669
+ functionAssociations: [
2670
+ {
2671
+ function: hostCopyFunction,
2672
+ eventType: import_aws_cloudfront.FunctionEventType.VIEWER_REQUEST
2673
+ }
2674
+ ],
2675
+ edgeLambdas: [originRequestEdgeLambda]
2676
+ }
2545
2677
  }
2546
2678
  };
2547
2679
  }
@@ -2595,7 +2727,7 @@ var PerBranchHostname = class extends import_constructs9.Construct {
2595
2727
  distributionId
2596
2728
  }
2597
2729
  );
2598
- this.record = new import_aws_route533.ARecord(this, "alias-record", {
2730
+ this.record = new import_aws_route533.ARecord(this, `alias-record-${props.hostname}`, {
2599
2731
  zone: props.hostedZone,
2600
2732
  recordName: props.hostname,
2601
2733
  target: import_aws_route533.RecordTarget.fromAlias(new import_aws_route53_targets2.CloudFrontTarget(distribution))
@@ -7301,7 +7433,7 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7301
7433
  integration
7302
7434
  });
7303
7435
  const apiPrefix = this.branchName === "main" ? `api` : `api-${this.childZonePrefix}`;
7304
- new import_aws_route535.ARecord(this, "api-a-record", {
7436
+ new import_aws_route535.ARecord(this, `api-a-record-${apiPrefix}`, {
7305
7437
  zone: hostedZone,
7306
7438
  recordName: apiPrefix,
7307
7439
  target: import_aws_route535.RecordTarget.fromAlias(
@@ -7434,7 +7566,6 @@ var OpenHiGraphqlService = _OpenHiGraphqlService;
7434
7566
  // src/services/open-hi-website-service.ts
7435
7567
  var import_config5 = __toESM(require_lib());
7436
7568
  var import_aws_s32 = require("aws-cdk-lib/aws-s3");
7437
- var OPENHI_PR_NUMBER_ENV_VAR = "OPENHI_PR_NUMBER";
7438
7569
  var SSM_PARAM_NAME_FULL_DOMAIN = "WEBSITE_FULL_DOMAIN";
7439
7570
  var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
7440
7571
  /**
@@ -7495,12 +7626,6 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
7495
7626
  this.props = props;
7496
7627
  this.validateConfig(props);
7497
7628
  const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
7498
- this.prNumber = this.resolvePrNumber(props);
7499
- if (!isReleaseBranch && this.prNumber === void 0) {
7500
- throw new Error(
7501
- `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.`
7502
- );
7503
- }
7504
7629
  const hostedZone = this.createHostedZone();
7505
7630
  this.fullDomain = this.computeFullDomain(hostedZone);
7506
7631
  const shouldCreateHostingInfra = props.createHostingInfrastructure ?? isReleaseBranch;
@@ -7555,32 +7680,12 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
7555
7680
  return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
7556
7681
  }
7557
7682
  /**
7558
- * Resolves the PR number from props or the `OPENHI_PR_NUMBER` env var.
7559
- * Returns `undefined` on release-branch deploys where no PR number is
7560
- * needed.
7561
- */
7562
- resolvePrNumber(props) {
7563
- if (props.prNumber !== void 0) {
7564
- return props.prNumber;
7565
- }
7566
- const raw = process.env[OPENHI_PR_NUMBER_ENV_VAR]?.trim();
7567
- if (!raw) {
7568
- return void 0;
7569
- }
7570
- const parsed = Number.parseInt(raw, 10);
7571
- if (!Number.isInteger(parsed) || parsed <= 0) {
7572
- throw new Error(
7573
- `${OPENHI_PR_NUMBER_ENV_VAR} must be a positive integer; got "${raw}".`
7574
- );
7575
- }
7576
- return parsed;
7577
- }
7578
- /**
7579
- * Computes the full website domain from `domainPrefix`, the PR number,
7580
- * and the child zone name. Release-branch deploys serve at
7581
- * `\<domainPrefix\>.\<zone\>` (e.g. `admin.dev.openhi.org`); every other
7582
- * deploy serves a per-PR preview at `\<domainPrefix\>-pr-\<N\>.\<zone\>`
7583
- * (e.g. `admin-pr-123.dev.openhi.org`).
7683
+ * Computes the full website domain from `domainPrefix`,
7684
+ * `childZonePrefix`, and the child zone name. Release-branch deploys
7685
+ * serve at `\<domainPrefix\>.\<zone\>` (e.g. `admin.dev.openhi.org`);
7686
+ * every other deploy serves a per-PR preview at
7687
+ * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>`
7688
+ * (e.g. `admin-feat-1093-patient-migration.dev.openhi.org`).
7584
7689
  */
7585
7690
  computeFullDomain(hostedZone) {
7586
7691
  const subDomain = this.computeSubDomain();
@@ -7592,16 +7697,20 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
7592
7697
  * key prefix passed to {@link StaticContent} so the upload prefix
7593
7698
  * always matches the served hostname.
7594
7699
  *
7595
- * Non-release deploys compose the per-PR slug from
7596
- * {@link PER_BRANCH_PREVIEW_PREFIX} so the per-PR S3 key prefix
7700
+ * Non-release deploys compose the per-PR slug as
7701
+ * `\<domainPrefix\>-\<childZonePrefix\>`, mirroring the REST API's
7702
+ * `api-\<childZonePrefix\>` convention. When `domainPrefix` is `admin`
7703
+ * (the only consumer today), the resulting sub-domain starts with
7704
+ * {@link PER_BRANCH_PREVIEW_PREFIX}, so the per-PR S3 key prefix
7597
7705
  * matches what `StaticHosting`'s lifecycle rule expires.
7598
7706
  */
7599
7707
  computeSubDomain() {
7600
7708
  const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
7709
+ const domainPrefix = this.props.domainPrefix ?? "www";
7601
7710
  if (isReleaseBranch) {
7602
- return this.props.domainPrefix ?? "www";
7711
+ return domainPrefix;
7603
7712
  }
7604
- return `${PER_BRANCH_PREVIEW_PREFIX}${this.prNumber}`;
7713
+ return `${domainPrefix}-${this.childZonePrefix}`;
7605
7714
  }
7606
7715
  /**
7607
7716
  * Creates the StaticHosting infrastructure (bucket + distribution +
@@ -7662,8 +7771,9 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
7662
7771
  * The S3 key prefix is `\<sub-domain\>.\<zone\>/\<contentDest\>` so the
7663
7772
  * upload location matches the Host-header-derived folder the Lambda@Edge
7664
7773
  * viewer-request handler prepends. Passing the zone name (rather than
7665
- * `this.fullDomain`) for the `fullDomain` prop keeps the prefix flat
7666
- * `admin-pr-123.dev.openhi.org/`, not `admin-pr-123.admin.dev.openhi.org/`.
7774
+ * `this.fullDomain`) for the `fullDomain` prop keeps the prefix flat
7775
+ * `admin-feat-foo.dev.openhi.org/`, not
7776
+ * `admin-feat-foo.admin.dev.openhi.org/`.
7667
7777
  */
7668
7778
  createStaticContent(bucket) {
7669
7779
  const { contentSourceDirectory, contentDestinationDirectory } = this.props;
@@ -7677,9 +7787,10 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
7677
7787
  }
7678
7788
  /**
7679
7789
  * Creates the per-PR `PerBranchHostname` alias record on non-release
7680
- * branch deploys. The record points `\<domainPrefix\>-pr-\<N\>.\<zone\>`
7681
- * at the release-branch CloudFront distribution (resolved from SSM
7682
- * against {@link OpenHiService.releaseBranchHash}).
7790
+ * branch deploys. The record points
7791
+ * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>` at the release-branch
7792
+ * CloudFront distribution (resolved from SSM against
7793
+ * {@link OpenHiService.releaseBranchHash}).
7683
7794
  */
7684
7795
  createPerBranchHostname(hostedZone) {
7685
7796
  return new PerBranchHostname(this, "per-branch-hostname", {
@@ -8242,7 +8353,6 @@ var RenameCascadeWorkflow = class extends import_constructs25.Construct {
8242
8353
  DataStorePostgresReplica,
8243
8354
  DiscoverableStringParameter,
8244
8355
  DynamoDbDataStore,
8245
- OPENHI_PR_NUMBER_ENV_VAR,
8246
8356
  OPENHI_REPO_TAG_KEY_ENV_VAR,
8247
8357
  OPENHI_RESOURCE_URN_SYSTEM,
8248
8358
  OPENHI_TAG_KEY_PREFIX_ENV_VAR,