@openhi/constructs 0.0.85 → 0.0.86

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
@@ -3,7 +3,7 @@ import {
3
3
  DATA_STORE_CHANGE_DETAIL_TYPE,
4
4
  DATA_STORE_CHANGE_EVENT_SOURCE,
5
5
  buildFhirCurrentResourceChangeDetail
6
- } from "./chunk-SWSN6GDD.mjs";
6
+ } from "./chunk-CEOAGPYY.mjs";
7
7
  import {
8
8
  __commonJS,
9
9
  __toESM
@@ -347,6 +347,10 @@ var OpenHiService = class extends Stack {
347
347
  this.environmentHash = environmentHash;
348
348
  this.branchHash = branchHash;
349
349
  this.stackHash = stackHash;
350
+ this.node.setContext(
351
+ `availability-zones:account=${account}:region=${region}`,
352
+ [`${region}a`, `${region}b`, `${region}c`]
353
+ );
350
354
  Tags.of(this).add(`${appName}:repo-name`, repoName.slice(0, 255));
351
355
  Tags.of(this).add(`${appName}:branch-name`, branchName.slice(0, 255));
352
356
  Tags.of(this).add(`${appName}:service-type`, id.slice(0, 255));
@@ -530,6 +534,46 @@ var _RootGraphqlApi = class _RootGraphqlApi extends GraphqlApi {
530
534
  _RootGraphqlApi.SSM_PARAM_NAME = "ROOT_GRAPHQL_API";
531
535
  var RootGraphqlApi = _RootGraphqlApi;
532
536
 
537
+ // src/components/cognito/cognito-fixture-seeder-client.ts
538
+ import { Duration } from "aws-cdk-lib";
539
+ import {
540
+ UserPoolClient
541
+ } from "aws-cdk-lib/aws-cognito";
542
+ var CognitoFixtureSeederClient = class extends UserPoolClient {
543
+ constructor(scope, props) {
544
+ const { userPool, ...rest } = props;
545
+ super(scope, "fixture-seeder-client", {
546
+ userPool,
547
+ generateSecret: false,
548
+ authFlows: {
549
+ userPassword: true
550
+ },
551
+ // No OAuth flows — the seeder calls Cognito's `InitiateAuth`
552
+ // directly with USER_PASSWORD_AUTH, not through the hosted-UI
553
+ // OAuth grant flows the SPA client uses. `disableOAuth: true`
554
+ // causes CDK to omit `AllowedOAuthFlowsUserPoolClient` entirely;
555
+ // passing an empty `oAuth` block instead still flips that flag on
556
+ // and Cognito rejects the create call for missing flows/scopes.
557
+ disableOAuth: true,
558
+ // Short-lived tokens: a seeder run takes seconds, not hours.
559
+ // 1h access-token validity is the minimum Cognito permits and is
560
+ // plenty for a fixture run.
561
+ accessTokenValidity: Duration.hours(1),
562
+ idTokenValidity: Duration.hours(1),
563
+ refreshTokenValidity: Duration.days(1),
564
+ preventUserExistenceErrors: true,
565
+ ...rest
566
+ });
567
+ }
568
+ };
569
+ /**
570
+ * SSM parameter name suffix used to publish this client's ID for
571
+ * cross-stack lookups. Built into a full parameter name via
572
+ * `buildParameterName` with `serviceType` AUTH (since the auth stack
573
+ * owns this resource).
574
+ */
575
+ CognitoFixtureSeederClient.SSM_PARAM_NAME = "COGNITO_FIXTURE_SEEDER_CLIENT";
576
+
533
577
  // src/components/cognito/cognito-user-pool.ts
534
578
  import {
535
579
  UserPool,
@@ -569,8 +613,8 @@ var CognitoUserPool = class extends UserPool {
569
613
  CognitoUserPool.SSM_PARAM_NAME = "COGNITO_USER_POOL";
570
614
 
571
615
  // src/components/cognito/cognito-user-pool-client.ts
572
- import { UserPoolClient } from "aws-cdk-lib/aws-cognito";
573
- var CognitoUserPoolClient = class extends UserPoolClient {
616
+ import { UserPoolClient as UserPoolClient2 } from "aws-cdk-lib/aws-cognito";
617
+ var CognitoUserPoolClient = class extends UserPoolClient2 {
574
618
  constructor(scope, props) {
575
619
  super(scope, "user-pool-client", {
576
620
  /**
@@ -658,7 +702,7 @@ var PreTokenGenerationLambda = class extends Construct {
658
702
  // src/components/dynamodb/data-store-historical-archive.ts
659
703
  import fs2 from "fs";
660
704
  import path2 from "path";
661
- import { Duration, RemovalPolicy as RemovalPolicy2, Size } from "aws-cdk-lib";
705
+ import { Duration as Duration2, RemovalPolicy as RemovalPolicy2, Size } from "aws-cdk-lib";
662
706
  import * as kinesisfirehose from "aws-cdk-lib/aws-kinesisfirehose";
663
707
  import { Runtime as Runtime2 } from "aws-cdk-lib/aws-lambda";
664
708
  import { NodejsFunction as NodejsFunction2 } from "aws-cdk-lib/aws-lambda-nodejs";
@@ -696,7 +740,7 @@ var DataStoreHistoricalArchive = class extends Construct2 {
696
740
  entry: resolveHandlerEntry2(__dirname),
697
741
  runtime: Runtime2.NODEJS_LATEST,
698
742
  memorySize: 512,
699
- timeout: Duration.minutes(1),
743
+ timeout: Duration2.minutes(1),
700
744
  description: "Firehose transform: filter CURRENT resource rows, S3 keys, EventBridge PutEvents",
701
745
  environment: props.dataEventBus && putEventsFailureDlqBucket ? {
702
746
  DATA_EVENT_BUS_NAME: props.dataEventBus.eventBusName,
@@ -712,14 +756,14 @@ var DataStoreHistoricalArchive = class extends Construct2 {
712
756
  const processor = new kinesisfirehose.LambdaFunctionProcessor(
713
757
  this.transformFunction,
714
758
  {
715
- bufferInterval: Duration.seconds(60),
759
+ bufferInterval: Duration2.seconds(60),
716
760
  bufferSize: Size.mebibytes(3),
717
761
  retries: 3
718
762
  }
719
763
  );
720
764
  const destination = new kinesisfirehose.S3Bucket(this.archiveBucket, {
721
765
  compression: kinesisfirehose.Compression.GZIP,
722
- bufferingInterval: Duration.seconds(300),
766
+ bufferingInterval: Duration2.seconds(300),
723
767
  // Firehose requires SizeInMBs ≥ 64 when dynamic partitioning is enabled.
724
768
  bufferingSize: Size.mebibytes(64),
725
769
  processors: [processor],
@@ -789,7 +833,15 @@ var DynamoDbDataStore = class extends Table {
789
833
  type: AttributeType.STRING
790
834
  },
791
835
  projectionType: ProjectionType.INCLUDE,
792
- nonKeyAttributes: ["srcType", "srcId", "path", "srcPk", "srcSk", "ts"]
836
+ nonKeyAttributes: [
837
+ "summary",
838
+ "vid",
839
+ "lastUpdated",
840
+ "createdDate",
841
+ "modifiedDate",
842
+ "createdById",
843
+ "modifiedById"
844
+ ]
793
845
  });
794
846
  this.addGlobalSecondaryIndex({
795
847
  indexName: "GSI2",
@@ -802,32 +854,12 @@ var DynamoDbDataStore = class extends Table {
802
854
  type: AttributeType.STRING
803
855
  },
804
856
  projectionType: ProjectionType.INCLUDE,
805
- nonKeyAttributes: ["resourcePk", "resourceSk", "display", "status"]
806
- });
807
- this.addGlobalSecondaryIndex({
808
- indexName: "GSI3",
809
- partitionKey: {
810
- name: "GSI3PK",
811
- type: AttributeType.STRING
812
- },
813
- sortKey: {
814
- name: "GSI3SK",
815
- type: AttributeType.STRING
816
- },
817
- projectionType: ProjectionType.INCLUDE,
818
- nonKeyAttributes: ["resourcePk", "resourceSk"]
819
- });
820
- this.addGlobalSecondaryIndex({
821
- indexName: "GSI4",
822
- partitionKey: {
823
- name: "GSI4PK",
824
- type: AttributeType.STRING
825
- },
826
- sortKey: {
827
- name: "GSI4SK",
828
- type: AttributeType.STRING
829
- },
830
- projectionType: ProjectionType.ALL
857
+ nonKeyAttributes: [
858
+ "id",
859
+ "currentTenant",
860
+ "currentWorkspace",
861
+ "displayName"
862
+ ]
831
863
  });
832
864
  }
833
865
  };
@@ -876,8 +908,172 @@ var OpsEventBus = class _OpsEventBus extends EventBus2 {
876
908
  }
877
909
  };
878
910
 
911
+ // src/components/postgres/data-store-postgres-replica.ts
912
+ import fs3 from "fs";
913
+ import path3 from "path";
914
+ import { Duration as Duration3, Stack as Stack2 } from "aws-cdk-lib";
915
+ import * as ec2 from "aws-cdk-lib/aws-ec2";
916
+ import { Runtime as Runtime3, StartingPosition } from "aws-cdk-lib/aws-lambda";
917
+ import { KinesisEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
918
+ import { NodejsFunction as NodejsFunction3 } from "aws-cdk-lib/aws-lambda-nodejs";
919
+ import * as rds from "aws-cdk-lib/aws-rds";
920
+ import { Construct as Construct3 } from "constructs";
921
+ var HANDLER_NAME3 = "data-store-postgres-replication.handler.js";
922
+ var DEFAULT_DATABASE_NAME = "openhi";
923
+ var SCHEMA_NAME_PATTERN = /^[a-z_][a-z0-9_]{0,62}$/;
924
+ var POSTGRES_REPLICA_CLUSTER_ARN_SSM_NAME = "POSTGRES_REPLICA_CLUSTER_ARN";
925
+ var POSTGRES_REPLICA_SECRET_ARN_SSM_NAME = "POSTGRES_REPLICA_SECRET_ARN";
926
+ var POSTGRES_REPLICA_DATABASE_NAME_SSM_NAME = "POSTGRES_REPLICA_DATABASE_NAME";
927
+ function resolveHandlerEntry3(dirname) {
928
+ const sameDir = path3.join(dirname, HANDLER_NAME3);
929
+ if (fs3.existsSync(sameDir)) {
930
+ return sameDir;
931
+ }
932
+ return path3.join(dirname, "..", "..", "..", "lib", HANDLER_NAME3);
933
+ }
934
+ function getPostgresReplicaSchemaName(branchHash) {
935
+ const candidate = `b_${branchHash.toLowerCase()}`;
936
+ if (!SCHEMA_NAME_PATTERN.test(candidate)) {
937
+ throw new Error(
938
+ `Branch hash ${JSON.stringify(branchHash)} produces an invalid Postgres schema name ${JSON.stringify(candidate)}; expected /[a-z_][a-z0-9_]{0,62}/.`
939
+ );
940
+ }
941
+ return candidate;
942
+ }
943
+ var DataStorePostgresReplica = class extends Construct3 {
944
+ /**
945
+ * Resolve the cluster ARN published by an upstream {@link DataStorePostgresReplica}.
946
+ * Use from any stack that needs to grant `rds-data:ExecuteStatement` against
947
+ * the cluster.
948
+ */
949
+ static clusterArnFromConstruct(scope) {
950
+ return DiscoverableStringParameter.valueForLookupName(scope, {
951
+ ssmParamName: POSTGRES_REPLICA_CLUSTER_ARN_SSM_NAME,
952
+ serviceType: "data"
953
+ });
954
+ }
955
+ /**
956
+ * Resolve the credentials secret ARN published by an upstream
957
+ * {@link DataStorePostgresReplica}. Use from any stack that needs to grant
958
+ * `secretsmanager:GetSecretValue` against the secret.
959
+ */
960
+ static secretArnFromConstruct(scope) {
961
+ return DiscoverableStringParameter.valueForLookupName(scope, {
962
+ ssmParamName: POSTGRES_REPLICA_SECRET_ARN_SSM_NAME,
963
+ serviceType: "data"
964
+ });
965
+ }
966
+ /**
967
+ * Resolve the database name published by an upstream
968
+ * {@link DataStorePostgresReplica}.
969
+ */
970
+ static databaseNameFromConstruct(scope) {
971
+ return DiscoverableStringParameter.valueForLookupName(scope, {
972
+ ssmParamName: POSTGRES_REPLICA_DATABASE_NAME_SSM_NAME,
973
+ serviceType: "data"
974
+ });
975
+ }
976
+ constructor(scope, id, props) {
977
+ super(scope, id);
978
+ this.databaseName = props.databaseName ?? DEFAULT_DATABASE_NAME;
979
+ this.schemaName = getPostgresReplicaSchemaName(props.branchHash);
980
+ const region = Stack2.of(this).region;
981
+ this.vpc = props.vpc ?? new ec2.Vpc(this, "Vpc", {
982
+ availabilityZones: [`${region}a`, `${region}b`],
983
+ natGateways: 0,
984
+ subnetConfiguration: [
985
+ {
986
+ name: "isolated",
987
+ subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
988
+ cidrMask: 24
989
+ }
990
+ ]
991
+ });
992
+ this.cluster = new rds.DatabaseCluster(this, "Cluster", {
993
+ clusterIdentifier: `openhi-dstore-pg-${props.stackHash}`,
994
+ engine: rds.DatabaseClusterEngine.auroraPostgres({
995
+ version: rds.AuroraPostgresEngineVersion.VER_16_4
996
+ }),
997
+ vpc: this.vpc,
998
+ vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
999
+ writer: rds.ClusterInstance.serverlessV2("writer"),
1000
+ serverlessV2MinCapacity: props.minCapacity ?? 1,
1001
+ serverlessV2MaxCapacity: props.maxCapacity ?? 2,
1002
+ defaultDatabaseName: this.databaseName,
1003
+ credentials: rds.Credentials.fromGeneratedSecret("openhi_admin"),
1004
+ storageEncrypted: true,
1005
+ removalPolicy: props.removalPolicy,
1006
+ // Phase 2 of ADR 2026-04-17-01: the REST API Lambda queries Postgres
1007
+ // via the RDS Data API (HTTPS) so it can stay out of the cluster's VPC.
1008
+ // Direct `pg` from the replication Lambda continues to work in parallel.
1009
+ enableDataApi: true
1010
+ });
1011
+ this.publishCoordinatesToSsm();
1012
+ this.replicationFunction = new NodejsFunction3(this, "ReplicationFunction", {
1013
+ entry: resolveHandlerEntry3(__dirname),
1014
+ runtime: Runtime3.NODEJS_LATEST,
1015
+ memorySize: 512,
1016
+ timeout: Duration3.minutes(1),
1017
+ vpc: this.vpc,
1018
+ vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
1019
+ description: "Replicates DynamoDB current-resource changes into the Postgres `resources` JSONB table (ADR 2026-04-17-01).",
1020
+ environment: {
1021
+ OPENHI_PG_HOST: this.cluster.clusterEndpoint.hostname,
1022
+ OPENHI_PG_PORT: this.cluster.clusterEndpoint.port.toString(),
1023
+ OPENHI_PG_DATABASE: this.databaseName,
1024
+ OPENHI_PG_SCHEMA: this.schemaName,
1025
+ OPENHI_PG_SECRET_ARN: this.cluster.secret.secretArn,
1026
+ OPENHI_PG_SSL: "true"
1027
+ },
1028
+ bundling: {
1029
+ minify: true,
1030
+ sourceMap: false,
1031
+ // pg has conditional/optional deps (pg-native, pg-cloudflare) that
1032
+ // historically misbehave when bundled by esbuild; keep it as a real
1033
+ // node_module in the Lambda zip instead.
1034
+ nodeModules: ["pg"]
1035
+ }
1036
+ });
1037
+ this.cluster.secret.grantRead(this.replicationFunction);
1038
+ this.cluster.connections.allowDefaultPortFrom(this.replicationFunction);
1039
+ this.replicationFunction.addEventSource(
1040
+ new KinesisEventSource(props.kinesisStream, {
1041
+ startingPosition: StartingPosition.LATEST,
1042
+ batchSize: 100,
1043
+ maxBatchingWindow: Duration3.seconds(5),
1044
+ retryAttempts: 10,
1045
+ bisectBatchOnError: true,
1046
+ parallelizationFactor: 2,
1047
+ reportBatchItemFailures: true
1048
+ })
1049
+ );
1050
+ }
1051
+ /**
1052
+ * Publishes the cluster ARN, secret ARN, and database name as discoverable
1053
+ * SSM parameters so the REST API stack (and any future read-side consumer)
1054
+ * can wire RDS Data API access without a direct CDK cross-stack reference.
1055
+ */
1056
+ publishCoordinatesToSsm() {
1057
+ new DiscoverableStringParameter(this, "cluster-arn-param", {
1058
+ ssmParamName: POSTGRES_REPLICA_CLUSTER_ARN_SSM_NAME,
1059
+ stringValue: this.cluster.clusterArn,
1060
+ description: "ARN of the Aurora Serverless v2 cluster backing the Postgres replication tier (ADR 2026-04-17-01)."
1061
+ });
1062
+ new DiscoverableStringParameter(this, "secret-arn-param", {
1063
+ ssmParamName: POSTGRES_REPLICA_SECRET_ARN_SSM_NAME,
1064
+ stringValue: this.cluster.secret.secretArn,
1065
+ description: "ARN of the Secrets Manager secret with credentials for the Postgres replication tier."
1066
+ });
1067
+ new DiscoverableStringParameter(this, "database-name-param", {
1068
+ ssmParamName: POSTGRES_REPLICA_DATABASE_NAME_SSM_NAME,
1069
+ stringValue: this.databaseName,
1070
+ description: "Database name within the Postgres replication cluster."
1071
+ });
1072
+ }
1073
+ };
1074
+
879
1075
  // src/components/route-53/child-hosted-zone.ts
880
- import { Duration as Duration2 } from "aws-cdk-lib";
1076
+ import { Duration as Duration4 } from "aws-cdk-lib";
881
1077
  import {
882
1078
  HostedZone,
883
1079
  NsRecord
@@ -889,7 +1085,7 @@ var ChildHostedZone = class extends HostedZone {
889
1085
  zone: props.parentHostedZone,
890
1086
  recordName: this.zoneName,
891
1087
  values: this.hostedZoneNameServers || [],
892
- ttl: Duration2.minutes(5)
1088
+ ttl: Duration4.minutes(5)
893
1089
  });
894
1090
  }
895
1091
  };
@@ -899,8 +1095,8 @@ var ChildHostedZone = class extends HostedZone {
899
1095
  ChildHostedZone.SSM_PARAM_NAME = "CHILDHOSTEDZONE";
900
1096
 
901
1097
  // src/components/route-53/root-hosted-zone.ts
902
- import { Construct as Construct3 } from "constructs";
903
- var RootHostedZone = class extends Construct3 {
1098
+ import { Construct as Construct4 } from "constructs";
1099
+ var RootHostedZone = class extends Construct4 {
904
1100
  };
905
1101
 
906
1102
  // src/components/static-hosting/static-hosting.ts
@@ -910,10 +1106,10 @@ import {
910
1106
  } from "aws-cdk-lib/aws-cloudfront";
911
1107
  import { S3BucketOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
912
1108
  import { Bucket as Bucket2 } from "aws-cdk-lib/aws-s3";
913
- import { Duration as Duration3 } from "aws-cdk-lib/core";
914
- import { Construct as Construct4 } from "constructs";
1109
+ import { Duration as Duration5 } from "aws-cdk-lib/core";
1110
+ import { Construct as Construct5 } from "constructs";
915
1111
  var STATIC_HOSTING_SERVICE_TYPE = "website";
916
- var _StaticHosting = class _StaticHosting extends Construct4 {
1112
+ var _StaticHosting = class _StaticHosting extends Construct5 {
917
1113
  constructor(scope, id, props = {}) {
918
1114
  super(scope, id);
919
1115
  const stack = OpenHiService.of(scope);
@@ -931,9 +1127,9 @@ var _StaticHosting = class _StaticHosting extends Construct4 {
931
1127
  const cachePolicy = new CachePolicy(this, "cache-policy", {
932
1128
  cachePolicyName: `static-hosting-10s-${stack.branchHash}`,
933
1129
  comment: "Low TTL (10s) for static hosting; no invalidation",
934
- defaultTtl: Duration3.seconds(10),
935
- minTtl: Duration3.seconds(0),
936
- maxTtl: Duration3.seconds(10)
1130
+ defaultTtl: Duration5.seconds(10),
1131
+ minTtl: Duration5.seconds(0),
1132
+ maxTtl: Duration5.seconds(10)
937
1133
  });
938
1134
  this.distribution = new Distribution(this, "distribution", {
939
1135
  defaultBehavior: {
@@ -965,10 +1161,11 @@ _StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ARN = "STATIC_HOSTING_DISTRIBUTION_AR
965
1161
  var StaticHosting = _StaticHosting;
966
1162
 
967
1163
  // src/services/open-hi-auth-service.ts
1164
+ var import_config4 = __toESM(require_lib());
968
1165
  import {
969
1166
  LambdaVersion,
970
1167
  UserPool as UserPool2,
971
- UserPoolClient as UserPoolClient2,
1168
+ UserPoolClient as UserPoolClient3,
972
1169
  UserPoolDomain as UserPoolDomain2,
973
1170
  UserPoolOperation
974
1171
  } from "aws-cdk-lib/aws-cognito";
@@ -995,12 +1192,33 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
995
1192
  serviceType: _OpenHiAuthService.SERVICE_TYPE
996
1193
  }
997
1194
  );
998
- return UserPoolClient2.fromUserPoolClientId(
1195
+ return UserPoolClient3.fromUserPoolClientId(
999
1196
  scope,
1000
1197
  "user-pool-client",
1001
1198
  userPoolClientId
1002
1199
  );
1003
1200
  }
1201
+ /**
1202
+ * Returns the dedicated fixture-seeder IUserPoolClient by looking up
1203
+ * its ID from SSM. Only non-prod auth stacks publish this parameter
1204
+ * (per the conditional in {@link createFixtureSeederClient}); calling
1205
+ * this against a prod-deployed stack will fail at lookup time.
1206
+ *
1207
+ * Consumed by `OpenHiRestApiService` (in non-prod) so the authorizer
1208
+ * accepts tokens issued by this client, and by the seed-fixtures CLI
1209
+ * to drive USER_PASSWORD_AUTH against this client's ID.
1210
+ */
1211
+ static fixtureSeederClientFromConstruct(scope) {
1212
+ const clientId = DiscoverableStringParameter.valueForLookupName(scope, {
1213
+ ssmParamName: CognitoFixtureSeederClient.SSM_PARAM_NAME,
1214
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
1215
+ });
1216
+ return UserPoolClient3.fromUserPoolClientId(
1217
+ scope,
1218
+ "fixture-seeder-client",
1219
+ clientId
1220
+ );
1221
+ }
1004
1222
  /**
1005
1223
  * Returns an IUserPoolDomain by looking up the Auth stack's User Pool Domain from SSM.
1006
1224
  */
@@ -1032,6 +1250,7 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1032
1250
  this.userPool = this.createUserPool();
1033
1251
  this.userPoolClient = this.createUserPoolClient();
1034
1252
  this.userPoolDomain = this.createUserPoolDomain();
1253
+ this.fixtureSeederClient = this.createFixtureSeederClient();
1035
1254
  }
1036
1255
  /**
1037
1256
  * Creates the KMS key for the Cognito User Pool and exports its ARN to SSM.
@@ -1093,6 +1312,31 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1093
1312
  });
1094
1313
  return client;
1095
1314
  }
1315
+ /**
1316
+ * Creates the dedicated USER_PASSWORD_AUTH app client for the
1317
+ * `@openhi/seed-fixtures` CLI, **only** in non-prod environments.
1318
+ * Returns `undefined` when this stack is being deployed to a prod
1319
+ * stage so the prod auth stack carries no fixture-seeder code path.
1320
+ *
1321
+ * Operator post-deploy: create a `fixture-seeder` Cognito user with
1322
+ * a service password (manually via console or scripted with
1323
+ * `aws cognito-idp admin-create-user`); the CLI consumes those creds
1324
+ * via env vars to drive `InitiateAuth`.
1325
+ */
1326
+ createFixtureSeederClient() {
1327
+ if (this.ohEnv.ohStage.stageType === import_config4.OPEN_HI_STAGE.PROD) {
1328
+ return void 0;
1329
+ }
1330
+ const client = new CognitoFixtureSeederClient(this, {
1331
+ userPool: this.userPool
1332
+ });
1333
+ new DiscoverableStringParameter(this, "fixture-seeder-client-param", {
1334
+ ssmParamName: CognitoFixtureSeederClient.SSM_PARAM_NAME,
1335
+ stringValue: client.userPoolClientId,
1336
+ description: "Cognito User Pool Client ID for the OpenHI fixture-seeder CLI (USER_PASSWORD_AUTH; non-prod only); cross-stack reference"
1337
+ });
1338
+ return client;
1339
+ }
1096
1340
  /**
1097
1341
  * Creates the User Pool Domain (Cognito hosted UI) and exports domain name to SSM.
1098
1342
  * Look up via {@link OpenHiAuthService.userPoolDomainFromConstruct}.
@@ -1224,6 +1468,7 @@ _OpenHiGlobalService.SERVICE_TYPE = "global";
1224
1468
  var OpenHiGlobalService = _OpenHiGlobalService;
1225
1469
 
1226
1470
  // src/services/open-hi-rest-api-service.ts
1471
+ var import_config5 = __toESM(require_lib());
1227
1472
  import {
1228
1473
  CorsHttpMethod,
1229
1474
  DomainName,
@@ -1242,7 +1487,7 @@ import {
1242
1487
  RecordTarget
1243
1488
  } from "aws-cdk-lib/aws-route53";
1244
1489
  import { ApiGatewayv2DomainProperties } from "aws-cdk-lib/aws-route53-targets";
1245
- import { Duration as Duration4 } from "aws-cdk-lib/core";
1490
+ import { Duration as Duration6 } from "aws-cdk-lib/core";
1246
1491
 
1247
1492
  // src/services/open-hi-data-service.ts
1248
1493
  import { StreamViewType, Table as Table2 } from "aws-cdk-lib/aws-dynamodb";
@@ -1288,7 +1533,11 @@ var _OpenHiDataService = class _OpenHiDataService extends OpenHiService {
1288
1533
  "data-store-change-stream",
1289
1534
  {
1290
1535
  streamName: `openhi-dstore-cdc-${this.branchHash}`,
1291
- streamMode: kinesis.StreamMode.ON_DEMAND
1536
+ streamMode: kinesis.StreamMode.ON_DEMAND,
1537
+ // CDK default for kinesis.Stream is RETAIN, which strands the stream
1538
+ // when a non-prod stack is destroyed. Use the service's policy so
1539
+ // non-prod tears down cleanly while prod retains.
1540
+ removalPolicy: this.removalPolicy
1292
1541
  }
1293
1542
  );
1294
1543
  this.dataStore = this.createDataStore();
@@ -1302,6 +1551,16 @@ var _OpenHiDataService = class _OpenHiDataService extends OpenHiService {
1302
1551
  dataEventBus: this.dataEventBus
1303
1552
  }
1304
1553
  );
1554
+ this.dataStorePostgresReplica = new DataStorePostgresReplica(
1555
+ this,
1556
+ "data-store-postgres-replica",
1557
+ {
1558
+ kinesisStream: this.dataStoreChangeStream,
1559
+ removalPolicy: this.removalPolicy,
1560
+ stackHash: this.stackHash,
1561
+ branchHash: this.branchHash
1562
+ }
1563
+ );
1305
1564
  }
1306
1565
  /**
1307
1566
  * Creates the data event bus.
@@ -1332,57 +1591,61 @@ _OpenHiDataService.SERVICE_TYPE = "data";
1332
1591
  var OpenHiDataService = _OpenHiDataService;
1333
1592
 
1334
1593
  // src/data/lambda/cors-options-lambda.ts
1335
- import fs3 from "fs";
1336
- import path3 from "path";
1337
- import { Runtime as Runtime3 } from "aws-cdk-lib/aws-lambda";
1338
- import { NodejsFunction as NodejsFunction3 } from "aws-cdk-lib/aws-lambda-nodejs";
1339
- import { Construct as Construct5 } from "constructs";
1340
- var HANDLER_NAME3 = "cors-options-lambda.handler.js";
1341
- function resolveHandlerEntry3(dirname) {
1342
- const sameDir = path3.join(dirname, HANDLER_NAME3);
1343
- if (fs3.existsSync(sameDir)) {
1594
+ import fs4 from "fs";
1595
+ import path4 from "path";
1596
+ import { Runtime as Runtime4 } from "aws-cdk-lib/aws-lambda";
1597
+ import { NodejsFunction as NodejsFunction4 } from "aws-cdk-lib/aws-lambda-nodejs";
1598
+ import { Construct as Construct6 } from "constructs";
1599
+ var HANDLER_NAME4 = "cors-options-lambda.handler.js";
1600
+ function resolveHandlerEntry4(dirname) {
1601
+ const sameDir = path4.join(dirname, HANDLER_NAME4);
1602
+ if (fs4.existsSync(sameDir)) {
1344
1603
  return sameDir;
1345
1604
  }
1346
- const fromLib = path3.join(dirname, "..", "..", "..", "lib", HANDLER_NAME3);
1605
+ const fromLib = path4.join(dirname, "..", "..", "..", "lib", HANDLER_NAME4);
1347
1606
  return fromLib;
1348
1607
  }
1349
- var CorsOptionsLambda = class extends Construct5 {
1608
+ var CorsOptionsLambda = class extends Construct6 {
1350
1609
  constructor(scope, id = "cors-options-lambda") {
1351
1610
  super(scope, id);
1352
- this.lambda = new NodejsFunction3(this, "handler", {
1353
- entry: resolveHandlerEntry3(__dirname),
1354
- runtime: Runtime3.NODEJS_LATEST,
1611
+ this.lambda = new NodejsFunction4(this, "handler", {
1612
+ entry: resolveHandlerEntry4(__dirname),
1613
+ runtime: Runtime4.NODEJS_LATEST,
1355
1614
  memorySize: 128
1356
1615
  });
1357
1616
  }
1358
1617
  };
1359
1618
 
1360
1619
  // src/data/lambda/rest-api-lambda.ts
1361
- import fs4 from "fs";
1362
- import path4 from "path";
1363
- import { Runtime as Runtime4 } from "aws-cdk-lib/aws-lambda";
1364
- import { NodejsFunction as NodejsFunction4 } from "aws-cdk-lib/aws-lambda-nodejs";
1365
- import { Construct as Construct6 } from "constructs";
1366
- var HANDLER_NAME4 = "rest-api-lambda.handler.js";
1367
- function resolveHandlerEntry4(dirname) {
1368
- const sameDir = path4.join(dirname, HANDLER_NAME4);
1369
- if (fs4.existsSync(sameDir)) {
1620
+ import fs5 from "fs";
1621
+ import path5 from "path";
1622
+ import { Runtime as Runtime5 } from "aws-cdk-lib/aws-lambda";
1623
+ import { NodejsFunction as NodejsFunction5 } from "aws-cdk-lib/aws-lambda-nodejs";
1624
+ import { Construct as Construct7 } from "constructs";
1625
+ var HANDLER_NAME5 = "rest-api-lambda.handler.js";
1626
+ function resolveHandlerEntry5(dirname) {
1627
+ const sameDir = path5.join(dirname, HANDLER_NAME5);
1628
+ if (fs5.existsSync(sameDir)) {
1370
1629
  return sameDir;
1371
1630
  }
1372
- const fromLib = path4.join(dirname, "..", "..", "..", "lib", HANDLER_NAME4);
1631
+ const fromLib = path5.join(dirname, "..", "..", "..", "lib", HANDLER_NAME5);
1373
1632
  return fromLib;
1374
1633
  }
1375
- var RestApiLambda = class extends Construct6 {
1634
+ var RestApiLambda = class extends Construct7 {
1376
1635
  constructor(scope, props) {
1377
1636
  super(scope, "rest-api-lambda");
1378
- this.lambda = new NodejsFunction4(this, "handler", {
1379
- entry: resolveHandlerEntry4(__dirname),
1380
- runtime: Runtime4.NODEJS_LATEST,
1637
+ this.lambda = new NodejsFunction5(this, "handler", {
1638
+ entry: resolveHandlerEntry5(__dirname),
1639
+ runtime: Runtime5.NODEJS_LATEST,
1381
1640
  memorySize: 1024,
1382
1641
  environment: {
1383
1642
  DYNAMO_TABLE_NAME: props.dynamoTableName,
1384
1643
  BRANCH_TAG_VALUE: props.branchTagValue,
1385
- HTTP_API_TAG_VALUE: props.httpApiTagValue
1644
+ HTTP_API_TAG_VALUE: props.httpApiTagValue,
1645
+ OPENHI_PG_CLUSTER_ARN: props.postgresClusterArn,
1646
+ OPENHI_PG_SECRET_ARN: props.postgresSecretArn,
1647
+ OPENHI_PG_DATABASE: props.postgresDatabase,
1648
+ OPENHI_PG_SCHEMA: props.postgresSchema
1386
1649
  },
1387
1650
  bundling: {
1388
1651
  minify: true,
@@ -1501,11 +1764,36 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
1501
1764
  */
1502
1765
  createRestApiLambdaAndRoutes(hostedZone, domainName) {
1503
1766
  const dataStoreTable = OpenHiDataService.dynamoDbDataStoreFromConstruct(this);
1767
+ const postgresClusterArn = DataStorePostgresReplica.clusterArnFromConstruct(this);
1768
+ const postgresSecretArn = DataStorePostgresReplica.secretArnFromConstruct(this);
1769
+ const postgresDatabase = DataStorePostgresReplica.databaseNameFromConstruct(this);
1770
+ const postgresSchema = getPostgresReplicaSchemaName(this.branchHash);
1504
1771
  const { lambda } = new RestApiLambda(this, {
1505
1772
  dynamoTableName: dataStoreTable.tableName,
1506
1773
  branchTagValue: this.branchName,
1507
- httpApiTagValue: RootHttpApi.SSM_PARAM_NAME
1774
+ httpApiTagValue: RootHttpApi.SSM_PARAM_NAME,
1775
+ postgresClusterArn,
1776
+ postgresSecretArn,
1777
+ postgresDatabase,
1778
+ postgresSchema
1508
1779
  });
1780
+ lambda.addToRolePolicy(
1781
+ new PolicyStatement({
1782
+ effect: Effect.ALLOW,
1783
+ actions: [
1784
+ "rds-data:ExecuteStatement",
1785
+ "rds-data:BatchExecuteStatement"
1786
+ ],
1787
+ resources: [postgresClusterArn]
1788
+ })
1789
+ );
1790
+ lambda.addToRolePolicy(
1791
+ new PolicyStatement({
1792
+ effect: Effect.ALLOW,
1793
+ actions: ["secretsmanager:GetSecretValue"],
1794
+ resources: [postgresSecretArn]
1795
+ })
1796
+ );
1509
1797
  const dynamoActions = [
1510
1798
  "dynamodb:GetItem",
1511
1799
  "dynamodb:Query",
@@ -1585,10 +1873,16 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
1585
1873
  createRootHttpApi(domainName) {
1586
1874
  const userPool = OpenHiAuthService.userPoolFromConstruct(this);
1587
1875
  const userPoolClient = OpenHiAuthService.userPoolClientFromConstruct(this);
1876
+ const userPoolClients = [userPoolClient];
1877
+ if (this.ohEnv.ohStage.stageType !== import_config5.OPEN_HI_STAGE.PROD) {
1878
+ userPoolClients.push(
1879
+ OpenHiAuthService.fixtureSeederClientFromConstruct(this)
1880
+ );
1881
+ }
1588
1882
  const cognitoAuthorizer = new HttpUserPoolAuthorizer(
1589
1883
  "cognito-authorizer",
1590
1884
  userPool,
1591
- { userPoolClients: [userPoolClient] }
1885
+ { userPoolClients }
1592
1886
  );
1593
1887
  const { corsPreflight: cors, ...restRootHttpApiProps } = this.props.rootHttpApiProps ?? {};
1594
1888
  const corsPreflight = cors !== void 0 ? {
@@ -1607,7 +1901,7 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
1607
1901
  "Authorization"
1608
1902
  ],
1609
1903
  allowCredentials: cors.allowCredentials ?? true,
1610
- maxAge: cors.maxAge ?? Duration4.days(1),
1904
+ maxAge: cors.maxAge ?? Duration6.days(1),
1611
1905
  ...cors.exposeHeaders !== void 0 && {
1612
1906
  exposeHeaders: cors.exposeHeaders
1613
1907
  }
@@ -1673,6 +1967,7 @@ _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
1673
1967
  var OpenHiGraphqlService = _OpenHiGraphqlService;
1674
1968
  export {
1675
1969
  ChildHostedZone,
1970
+ CognitoFixtureSeederClient,
1676
1971
  CognitoUserPool,
1677
1972
  CognitoUserPoolClient,
1678
1973
  CognitoUserPoolDomain,
@@ -1682,6 +1977,7 @@ export {
1682
1977
  DATA_STORE_CHANGE_EVENT_SOURCE,
1683
1978
  DataEventBus,
1684
1979
  DataStoreHistoricalArchive,
1980
+ DataStorePostgresReplica,
1685
1981
  DiscoverableStringParameter,
1686
1982
  DynamoDbDataStore,
1687
1983
  OpenHiApp,
@@ -1694,6 +1990,9 @@ export {
1694
1990
  OpenHiService,
1695
1991
  OpenHiStage,
1696
1992
  OpsEventBus,
1993
+ POSTGRES_REPLICA_CLUSTER_ARN_SSM_NAME,
1994
+ POSTGRES_REPLICA_DATABASE_NAME_SSM_NAME,
1995
+ POSTGRES_REPLICA_SECRET_ARN_SSM_NAME,
1697
1996
  PreTokenGenerationLambda,
1698
1997
  REST_API_BASE_URL_SSM_NAME,
1699
1998
  RootGraphqlApi,
@@ -1703,6 +2002,7 @@ export {
1703
2002
  STATIC_HOSTING_SERVICE_TYPE,
1704
2003
  StaticHosting,
1705
2004
  buildFhirCurrentResourceChangeDetail,
1706
- getDynamoDbDataStoreTableName
2005
+ getDynamoDbDataStoreTableName,
2006
+ getPostgresReplicaSchemaName
1707
2007
  };
1708
2008
  //# sourceMappingURL=index.mjs.map