@medplum/cli 2.0.25 → 2.0.27
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/dist/esm/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var Z=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,o)=>(typeof require<"u"?require:t)[o]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});import{MEDPLUM_VERSION as yo,normalizeErrorString as ho}from"@medplum/core";import{Command as So}from"commander";import wo from"dotenv";import{getDisplayString as We,normalizeErrorString as _e}from"@medplum/core";import{platform as Ke}from"os";import{exec as qe}from"child_process";import{createServer as ze}from"http";import{Command as Te}from"commander";function m(e){return new Te(e).option("--client-id <clientId>","FHIR server client id").option("--client-secret <clientSecret>","FHIR server client secret").option("--base-url <baseUrl>","FHIR server base url").option("--token-url <tokenUrl>","FHIR server token url").option("--authorize-url <authorizeUrl>","FHIR server authorize url").option("--fhir-url-path <fhirUrlPath>","FHIR server url path")}import{MedplumClient as Le}from"@medplum/core";import{ClientStorage as Be}from"@medplum/core";import{existsSync as Q,mkdirSync as Ue,readFileSync as Fe,writeFileSync as je}from"fs";import{homedir as $e}from"os";import{resolve as ee}from"path";var E=class extends Be{constructor(){super();this.dirName=ee($e(),".medplum"),this.fileName=ee(this.dirName,"credentials")}clear(){this.writeFile({})}getString(o){return this.readFile()?.[o]}setString(o,n){let a=this.readFile()||{};n?a[o]=n:delete a[o],this.writeFile(a)}readFile(){if(Q(this.fileName))return JSON.parse(Fe(this.fileName,"utf8"))}writeFile(o){Q(this.dirName)||Ue(this.dirName),je(this.fileName,JSON.stringify(o,null,2),"utf8")}};async function l(e){let t=e.baseUrl??process.env.MEDPLUM_BASE_URL??"https://api.medplum.com/",o=e.fhirUrlPath??process.env.MEDPLUM_FHIR_URL_PATH??"",n=e.accessToken??process.env.MEDPLUM_CLIENT_ACCESS_TOKEN??"",a=e.tokenUrl??process.env.MEDPLUM_TOKEN_URL??"",i=e.authorizeUrl??process.env.MEDPLUM_AUTHORIZE_URL??"",s=e.fetch??fetch,c=new Le({fetch:s,baseUrl:t,tokenUrl:a,fhirUrlPath:o,authorizeUrl:i,storage:new E,onUnauthenticated:Oe});n&&c.setAccessToken(n);let u=e.clientId||process.env.MEDPLUM_CLIENT_ID,g=e.clientSecret||process.env.MEDPLUM_CLIENT_SECRET;return u&&g&&(c.setBasicAuth(u,g),await c.startClientLogin(u,g)),c}function Oe(){console.log("Unauthenticated: run `npx medplum login` to sign in")}var te="medplum-cli",oe="http://localhost:9615",I=m("login"),R=m("whoami");I.action(async e=>{let t=await l(e);await Ge(t)});R.action(async e=>{let t=await l(e);Ye(t)});async function Ge(e){await He(e);let t=new URL(e.getAuthorizeUrl());t.searchParams.set("client_id",te),t.searchParams.set("redirect_uri",oe),t.searchParams.set("scope","openid"),t.searchParams.set("response_type","code"),t.searchParams.set("prompt","login"),await Ve(t.toString())}async function He(e){let t=ze(async(o,n)=>{let a=new URL(o.url,"http://localhost:9615"),i=a.searchParams.get("code");if(a.pathname==="/"&&i)try{let s=await e.processCode(i,{clientId:te,redirectUri:oe});n.writeHead(200,{"Content-Type":"text/plain"}),n.end(`Signed in as ${We(s)}. You may close this window.`)}catch(s){n.writeHead(400,{"Content-Type":"text/plain"}),n.end(`Error: ${_e(s)}`)}finally{t.close()}else n.writeHead(404,{"Content-Type":"text/plain"}),n.end("Not found")}).listen(9615)}async function Ve(e){let t=Ke(),o;switch(t){case"openbsd":case"linux":o=`xdg-open '${e}'`;break;case"darwin":o=`open '${e}'`;break;case"win32":o=`cmd /c start "" "${e}"`;break;default:throw new Error("Unsupported platform: "+t)}qe(o)}function Ye(e){let t=e.getActiveLogin();t?(console.log(`Server: ${e.getBaseUrl()}`),console.log(`Profile: ${t.profile.display} (${t.profile.reference})`),console.log(`Project: ${t.project.display} (${t.project.reference})`)):console.log("Not logged in")}import{Command as Qt}from"commander";import{CloudFormationClient as Je,DescribeStackResourcesCommand as Xe,DescribeStacksCommand as Ze,ListStacksCommand as Qe}from"@aws-sdk/client-cloudformation";import{CloudFrontClient as et}from"@aws-sdk/client-cloudfront";import{ECSClient as tt}from"@aws-sdk/client-ecs";import{S3Client as ot}from"@aws-sdk/client-s3";var T=new Je({}),ne=new et({}),re=new tt({}),ae=new ot({}),nt="medplum:environment";async function B(){return(await T.send(new Qe({}))).StackSummaries?.filter(t=>t.StackName&&t.StackStatus!=="DELETE_COMPLETE")||[]}async function S(e){let t=await B();for(let o of t){let n=o.StackName,a=await U(n);if(a?.tag===e)return a}}async function U(e){let t=new Ze({StackName:e}),n=(await T.send(t))?.Stacks?.[0],a=n?.Tags?.find(c=>c.Key===nt);if(!a)return;let i=await T.send(new Xe({StackName:e}));if(!i.StackResources)return;let s={stack:n,tag:a.Value};for(let c of i.StackResources)c.ResourceType==="AWS::ECS::Cluster"?s.ecsCluster=c:c.ResourceType==="AWS::ECS::Service"?s.ecsService=c:c.ResourceType==="AWS::S3::Bucket"&&c.LogicalResourceId?.startsWith("FrontEndAppBucket")?s.appBucket=c:c.ResourceType==="AWS::S3::Bucket"&&c.LogicalResourceId?.startsWith("StorageStorageBucket")?s.storageBucket=c:c.ResourceType==="AWS::CloudFront::Distribution"&&c.LogicalResourceId?.startsWith("FrontEndAppDistribution")&&(s.appDistribution=c);return s}function M(e){console.log(`Medplum Tag: ${e.tag}`),console.log(`Stack Name: ${e.stack.StackName}`),console.log(`Stack ID: ${e.stack.StackId}`),console.log(`Status: ${e.stack.StackStatus}`),console.log(`ECS Cluster: ${e.ecsCluster?.PhysicalResourceId}`),console.log(`ECS Service: ${F(e.ecsService)}`),console.log(`App Bucket: ${e.appBucket?.PhysicalResourceId}`),console.log(`Storage Bucket: ${e.storageBucket?.PhysicalResourceId}`)}function F(e){return e?.PhysicalResourceId?.split("/")?.pop()||""}async function ie(e){let t=await S(e);if(!t){console.log("Stack not found");return}M(t)}import{ACMClient as ce,ListCertificatesCommand as rt,RequestCertificateCommand as at}from"@aws-sdk/client-acm";import{GetParameterCommand as it,PutParameterCommand as st,SSMClient as ct}from"@aws-sdk/client-ssm";import{GetCallerIdentityCommand as mt,STSClient as lt}from"@aws-sdk/client-sts";import{generateKeyPairSync as dt,randomUUID as ut}from"crypto";import{existsSync as pt,writeFileSync as ft}from"fs";import{resolve as gt}from"path";import yt from"readline";var ht=e=>`${e}DomainName`,me=e=>`${e}SslCertArn`,P;async function le(){let e={apiPort:8103,region:"us-east-1"};P=yt.createInterface({input:process.stdin,output:process.stdout}),d("MEDPLUM"),r("This tool prepares the necessary prerequisites for deploying Medplum in your AWS account."),r(""),r("Most Medplum infrastructure is deployed using the AWS CDK."),r("However, some AWS resources must be created manually, such as email addresses and SSL certificates."),r("This tool will help you create those resources."),r(""),r("Upon completion, this tool will:"),r(" 1. Generate a Medplum CDK config file (i.e., medplum.demo.config.json)"),r(" 2. Optionally generate an AWS CloudFront signing key"),r(" 3. Optionally request SSL certificates from AWS Certificate Manager"),r(" 4. Optionally write server config settings to AWS Parameter Store"),r(""),r("The Medplum infra config file is an input to the Medplum CDK."),r("The Medplum CDK will create and manage the necessary AWS resources."),r(""),r("We will ask a series of questions to generate your infra config file."),r("Some questions have predefined options in [square brackets]."),r("Some questions have default values in (parentheses), which you can accept by pressing Enter."),r("Press Ctrl+C at any time to exit.");let t=await St(e.region);t||(r("It appears that you do not have AWS credentials configured."),r("AWS credentials are not strictly required, but will enable some additional features."),r("If you intend to use AWS credentials, please configure them now."),await k("Do you want to continue without AWS credentials?")),d("ENVIRONMENT NAME"),r('Medplum deployments have a short environment name such as "prod", "staging", "alice", or "demo".'),r("The environment name is used in multiple places:"),r(" 1. As part of config file names (i.e., medplum.demo.config.json)"),r(" 2. As the base of CloudFormation stack names (i.e., MedplumDemo)"),r(" 3. AWS Parameter Store keys (i.e., /medplum/demo/...)"),e.name=await f("What is your environment name?","demo"),r('Using environment name "'+e.name+'"...'),d("CONFIG FILE"),r("Medplum Infrastructure will create a config file in the current directory.");let o=await f("What is the config file name?",`medplum.${e.name}.config.json`);pt(o)&&(r("Config file already exists."),await k("Do you want to overwrite the config file?")),r('Using config file "'+o+'"...'),p(o,e),d("AWS REGION"),r("Most Medplum resources will be created in a single AWS region."),e.region=await f("Enter your AWS region:","us-east-1"),p(o,e),d("AWS ACCOUNT NUMBER"),r("Medplum Infrastructure will use your AWS account number to create AWS resources."),t&&r("Using the AWS CLI, your current account ID is: "+t),e.accountNumber=await f("What is your AWS account number?",t),p(o,e),d("STACK NAME"),r("Medplum will create a CloudFormation stack to manage AWS resources."),r("AWS CloudFormation stack names ");let n="Medplum"+e.name.charAt(0).toUpperCase()+e.name.slice(1);for(e.stackName=await f("Enter your CloudFormation stack name?",n),p(o,e),d("BASE DOMAIN NAME"),r("Please enter the base domain name for your Medplum deployment."),r(""),r("Medplum deploys multiple subdomains for various services."),r(""),r('For example, "api." for the REST API and "app." for the web application.'),r("The base domain name is the common suffix for all subdomains."),r(""),r('For example, if your base domain name is "example.com",'),r('then the REST API will be "api.example.com".'),r(""),r('The base domain should include the TLD (i.e., ".com", ".org", ".net").'),r(""),r("Note that you must own the base domain, and it must use Route53 DNS.");!e.domainName;)e.domainName=await f("Enter your base domain name:");p(o,e),d("SUPPORT EMAIL"),r("Medplum sends transactional emails to users."),r("For example, emails to new users or for password reset."),r("Medplum will use the support email address to send these emails."),r("Note that you must verify the support email address in SES.");let a=await f("Enter your support email address:");d("API DOMAIN NAME"),r("Medplum deploys a REST API for the backend services."),e.apiDomainName=await f("Enter your REST API domain name:","api."+e.domainName),p(o,e),d("APP DOMAIN NAME"),r("Medplum deploys a web application for the user interface."),e.appDomainName=await f("Enter your web application domain name:","app."+e.domainName),p(o,e),d("STORAGE DOMAIN NAME"),r("Medplum deploys a storage service for file uploads."),e.storageDomainName=await f("Enter your storage domain name:","storage."+e.domainName),p(o,e),d("STORAGE BUCKET"),r("Medplum uses an S3 bucket to store binary content such as file uploads."),r("Medplum will create a the S3 bucket as part of the CloudFormation stack."),e.storageBucketName=await f("Enter your storage bucket name:","medplum-"+e.name+"-storage"),p(o,e),d("MAX AVAILABILITY ZONES"),r("Medplum API servers can be deployed in multiple availability zones."),r("This provides redundancy and high availability."),r("However, it also increases the cost of the deployment."),r("If you want to use all availability zones, choose a large number such as 99."),r("If you want to restrict the number, for example to manage EIP limits,"),r("then choose a small number such as 1 or 2."),e.maxAzs=await w("Enter the maximum number of availability zones:",[1,2,3,99],2),d("DATABASE INSTANCES"),r("Medplum uses a relational database to store data."),r("You can set up your own database,"),r("or Medplum can create a new RDS database as part of the CloudFormation stack."),await $("Do you want to create a new RDS database as part of the CloudFormation stack?")?(r("Medplum will create a new RDS database as part of the CloudFormation stack."),r(""),r("If you need high availability, you can choose multiple instances."),r("Use 1 for a single instance, or 2 for a primary and a standby."),e.rdsInstances=await w("Enter the number of database instances:",[1,2],1)):(r("Medplum will not create a new RDS database."),r("Please create a new RDS database and enter the database name, username, and password."),r('Set the AWS Secrets Manager secret ARN in the config file in the "rdsSecretsArn" setting.'),e.rdsSecretsArn="TODO"),p(o,e),d("SERVER INSTANCES"),r("Medplum uses AWS Fargate to run the API servers."),r("Medplum will create a new Fargate cluster as part of the CloudFormation stack."),r("Fargate will automatically scale the number of servers up and down."),r("If you need high availability, you can choose multiple instances."),e.desiredServerCount=await w("Enter the number of server instances:",[1,2,3,4,6,8],1),p(o,e),d("SERVER MEMORY"),r("You can choose the amount of memory for each server instance."),r("The default is 512 MB, which is sufficient for getting started."),r("Note that only certain CPU units are compatible with memory units."),r('Consult AWS Fargate "Task Definition Parameters" for more information.'),e.serverMemory=await w("Enter the server memory (MB):",[512,1024,2048,4096,8192,16384],512),p(o,e),d("SERVER CPU"),r("You can choose the amount of CPU for each server instance."),r("CPU is expressed as an integer using AWS CPU units"),r("The default is 256, which is sufficient for getting started."),r("Note that only certain CPU units are compatible with memory units."),r('Consult AWS Fargate "Task Definition Parameters" for more information.'),e.serverCpu=await w("Enter the server CPU:",[256,512,1024,2048,4096,8192,16384],256),p(o,e),d("SERVER IMAGE"),r("Medplum uses Docker images for the API servers."),r("You can choose the image to use for the servers."),r("Docker images can be loaded from either Docker Hub or AWS ECR."),r("The default is the latest Medplum release."),e.serverImage=await f("Enter the server image:","medplum/medplum-server:latest"),p(o,e),d("SIGNING KEY"),r("Medplum uses AWS CloudFront Presigned URLs for binary content such as file uploads.");let{privateKey:i,publicKey:s,passphrase:c}=bt();e.storagePublicKey=s,p(o,e),d("SSL CERTIFICATES"),r("Medplum will now check for existing SSL certificates for the subdomains.");let u=await wt(e.region);r("Found "+u.length+" certificate(s).");for(let{region:N,certName:v}of[{region:e.region,certName:"api"},{region:"us-east-1",certName:"app"},{region:"us-east-1",certName:"storage"}]){r("");let b=await Ct(e,u,N,v);e[me(v)]=b,p(o,e)}d("AWS PARAMETER STORE"),r("Medplum uses AWS Parameter Store to store sensitive configuration values."),r("These values will be encrypted at rest."),r(`The values will be stored in the "/medplum/${e.name}" path.`);let g={port:e.apiPort,baseUrl:`https://${e.apiDomainName}/`,appBaseUrl:`https://${e.appDomainName}/`,storageBaseUrl:`https://${e.storageDomainName}/binary/`,binaryStorage:`s3:${e.storageBucketName}`,signingKey:i,signingKeyPassphrase:c,supportEmail:a};r(JSON.stringify({...g,signingKey:"****",signingKeyPassphrase:"****"},null,2)),await k("Do you want to store these values in AWS Parameter Store?"),await kt(e.region,`/medplum/${e.name}/`,g),d("DONE!"),r("Medplum configuration complete."),r("You can now proceed to deploying the Medplum infrastructure with CDK."),r("Run:"),r(""),r(` npx cdk bootstrap -c config=${o}`),r(` npx cdk synth -c config=${o}`),e.region==="us-east-1"?r(` npx cdk deploy -c config=${o}`):r(` npx cdk deploy -c config=${o} --all`),r(""),r("See Medplum documentation for more information:"),r(""),r(" https://www.medplum.com/docs/self-hosting/install-on-aws"),r(""),P.close()}function r(e){P.write(e+`
|
|
2
|
+
var Z=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,o)=>(typeof require<"u"?require:t)[o]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{MEDPLUM_VERSION as xo,normalizeErrorString as No}from"@medplum/core";import{Command as Ao}from"commander";import Do from"dotenv";import{getDisplayString as Ve,normalizeErrorString as Je}from"@medplum/core";import{platform as Ye}from"os";import{exec as Xe}from"child_process";import{createServer as Ze}from"http";import{Command as Oe,Option as $e}from"commander";function m(e){return new Oe(e).option("--client-id <clientId>","FHIR server client id").option("--client-secret <clientSecret>","FHIR server client secret").option("--base-url <baseUrl>","FHIR server base url").option("--token-url <tokenUrl>","FHIR server token url").option("--authorize-url <authorizeUrl>","FHIR server authorize url").option("--fhir-url-path <fhirUrlPath>","FHIR server url path").addOption(new $e("--auth-type <authType>","Type of authentication").choices(["basic","client_credentials","authorization_code","jwt-bearer","token-exchange"]))}import{MedplumClient as ze}from"@medplum/core";import{ClientStorage as Le}from"@medplum/core";import{existsSync as Q,mkdirSync as We,readFileSync as _e,writeFileSync as Ke}from"fs";import{homedir as qe}from"os";import{resolve as ee}from"path";var y=class extends Le{constructor(o){super();this.dirName=ee(qe(),".medplum"),this.fileName=ee(this.dirName,o+".json")}clear(){this.writeFile({})}getString(o){return this.readFile()?.[o]}setString(o,n){let a=this.readFile()??{};n?a[o]=n:delete a[o],this.writeFile(a)}getObject(o){let n=this.getString(o);return n?JSON.parse(n):void 0}setObject(o,n){this.setString(o,n?JSON.stringify(n):void 0)}readFile(){if(Q(this.fileName))return JSON.parse(_e(this.fileName,"utf8"))}writeFile(o){Q(this.dirName)||We(this.dirName),Ke(this.fileName,JSON.stringify(o,null,2),"utf8")}};async function l(e,t){let o=t??"default",n=new y(o),{baseUrl:a,fhirUrlPath:i,accessToken:s,tokenUrl:c,authorizeUrl:u,fetchApi:g}=Ge(e,n),S=new ze({fetch:g,baseUrl:a,tokenUrl:c,fhirUrlPath:i,authorizeUrl:u,storage:n,onUnauthenticated:He});s&&S.setAccessToken(s);let w=e.clientId||process.env.MEDPLUM_CLIENT_ID,C=e.clientSecret||process.env.MEDPLUM_CLIENT_SECRET;return w&&C&&(S.setBasicAuth(w,C),await S.startClientLogin(w,C)),S}function Ge(e,t){let o=e.baseUrl??t.getString("baseUrl")??process.env.MEDPLUM_BASE_URL??"https://api.medplum.com/",n=e.fhirUrlPath??t.getString("fhirUrlPath")??process.env.MEDPLUM_FHIR_URL_PATH??"",a=e.accessToken??t.getString("accessToken")??process.env.MEDPLUM_CLIENT_ACCESS_TOKEN??"",i=e.tokenUrl??t.getString("tokenUrl")??process.env.MEDPLUM_TOKEN_URL??"",s=e.authorizeUrl??t.getString("authorizeUrl")??process.env.MEDPLUM_AUTHORIZE_URL??"",c=e.fetch??fetch;return{baseUrl:o,fhirUrlPath:n,accessToken:a,tokenUrl:i,authorizeUrl:s,fetchApi:c}}function He(){console.log("Unauthenticated: run `npx medplum login` to sign in")}var te="medplum-cli",oe="http://localhost:9615",I=m("login"),R=m("whoami");I.action(async e=>{let t=await l(e);await Qe(t)});R.action(async e=>{let t=await l(e);ot(t)});async function Qe(e){await et(e);let t=new URL(e.getAuthorizeUrl());t.searchParams.set("client_id",te),t.searchParams.set("redirect_uri",oe),t.searchParams.set("scope","openid"),t.searchParams.set("response_type","code"),t.searchParams.set("prompt","login"),await tt(t.toString())}async function et(e){let t=Ze(async(o,n)=>{let a=new URL(o.url,"http://localhost:9615"),i=a.searchParams.get("code");if(a.pathname==="/"&&i)try{let s=await e.processCode(i,{clientId:te,redirectUri:oe});n.writeHead(200,{"Content-Type":"text/plain"}),n.end(`Signed in as ${Ve(s)}. You may close this window.`)}catch(s){n.writeHead(400,{"Content-Type":"text/plain"}),n.end(`Error: ${Je(s)}`)}finally{t.close()}else n.writeHead(404,{"Content-Type":"text/plain"}),n.end("Not found")}).listen(9615)}async function tt(e){let t=Ye(),o;switch(t){case"openbsd":case"linux":o=`xdg-open '${e}'`;break;case"darwin":o=`open '${e}'`;break;case"win32":o=`cmd /c start "" "${e}"`;break;default:throw new Error("Unsupported platform: "+t)}Xe(o)}function ot(e){let t=e.getActiveLogin();t?(console.log(`Server: ${e.getBaseUrl()}`),console.log(`Profile: ${t.profile.display} (${t.profile.reference})`),console.log(`Project: ${t.project.display} (${t.project.reference})`)):console.log("Not logged in")}import{Command as io}from"commander";import{CloudFormationClient as nt,DescribeStackResourcesCommand as rt,DescribeStacksCommand as at,ListStacksCommand as it}from"@aws-sdk/client-cloudformation";import{CloudFrontClient as st}from"@aws-sdk/client-cloudfront";import{ECSClient as ct}from"@aws-sdk/client-ecs";import{S3Client as mt}from"@aws-sdk/client-s3";var T=new nt({}),ne=new st({}),re=new ct({}),ae=new mt({}),lt="medplum:environment";async function U(){return(await T.send(new it({}))).StackSummaries?.filter(t=>t.StackName&&t.StackStatus!=="DELETE_COMPLETE")||[]}async function v(e){let t=await U();for(let o of t){let n=o.StackName,a=await B(n);if(a?.tag===e)return a}}async function B(e){let t=new at({StackName:e}),n=(await T.send(t))?.Stacks?.[0],a=n?.Tags?.find(c=>c.Key===lt);if(!a)return;let i=await T.send(new rt({StackName:e}));if(!i.StackResources)return;let s={stack:n,tag:a.Value};for(let c of i.StackResources)c.ResourceType==="AWS::ECS::Cluster"?s.ecsCluster=c:c.ResourceType==="AWS::ECS::Service"?s.ecsService=c:c.ResourceType==="AWS::S3::Bucket"&&c.LogicalResourceId?.startsWith("FrontEndAppBucket")?s.appBucket=c:c.ResourceType==="AWS::S3::Bucket"&&c.LogicalResourceId?.startsWith("StorageStorageBucket")?s.storageBucket=c:c.ResourceType==="AWS::CloudFront::Distribution"&&c.LogicalResourceId?.startsWith("FrontEndAppDistribution")&&(s.appDistribution=c);return s}function k(e){console.log(`Medplum Tag: ${e.tag}`),console.log(`Stack Name: ${e.stack.StackName}`),console.log(`Stack ID: ${e.stack.StackId}`),console.log(`Status: ${e.stack.StackStatus}`),console.log(`ECS Cluster: ${e.ecsCluster?.PhysicalResourceId}`),console.log(`ECS Service: ${j(e.ecsService)}`),console.log(`App Bucket: ${e.appBucket?.PhysicalResourceId}`),console.log(`Storage Bucket: ${e.storageBucket?.PhysicalResourceId}`)}function j(e){return e?.PhysicalResourceId?.split("/")?.pop()||""}async function ie(e){let t=await v(e);if(!t){console.log("Stack not found");return}k(t)}import{ACMClient as ce,ListCertificatesCommand as dt,RequestCertificateCommand as ut}from"@aws-sdk/client-acm";import{GetParameterCommand as pt,PutParameterCommand as ft,SSMClient as gt}from"@aws-sdk/client-ssm";import{GetCallerIdentityCommand as yt,STSClient as ht}from"@aws-sdk/client-sts";import{generateKeyPairSync as St,randomUUID as wt}from"crypto";import{existsSync as Ct,writeFileSync as bt}from"fs";import{resolve as vt}from"path";import Et from"readline";var Mt=e=>`${e}DomainName`,me=e=>`${e}SslCertArn`,x;async function le(){let e={apiPort:8103,region:"us-east-1"};x=Et.createInterface({input:process.stdin,output:process.stdout}),d("MEDPLUM"),r("This tool prepares the necessary prerequisites for deploying Medplum in your AWS account."),r(""),r("Most Medplum infrastructure is deployed using the AWS CDK."),r("However, some AWS resources must be created manually, such as email addresses and SSL certificates."),r("This tool will help you create those resources."),r(""),r("Upon completion, this tool will:"),r(" 1. Generate a Medplum CDK config file (i.e., medplum.demo.config.json)"),r(" 2. Optionally generate an AWS CloudFront signing key"),r(" 3. Optionally request SSL certificates from AWS Certificate Manager"),r(" 4. Optionally write server config settings to AWS Parameter Store"),r(""),r("The Medplum infra config file is an input to the Medplum CDK."),r("The Medplum CDK will create and manage the necessary AWS resources."),r(""),r("We will ask a series of questions to generate your infra config file."),r("Some questions have predefined options in [square brackets]."),r("Some questions have default values in (parentheses), which you can accept by pressing Enter."),r("Press Ctrl+C at any time to exit.");let t=await kt(e.region);t||(r("It appears that you do not have AWS credentials configured."),r("AWS credentials are not strictly required, but will enable some additional features."),r("If you intend to use AWS credentials, please configure them now."),await P("Do you want to continue without AWS credentials?")),d("ENVIRONMENT NAME"),r('Medplum deployments have a short environment name such as "prod", "staging", "alice", or "demo".'),r("The environment name is used in multiple places:"),r(" 1. As part of config file names (i.e., medplum.demo.config.json)"),r(" 2. As the base of CloudFormation stack names (i.e., MedplumDemo)"),r(" 3. AWS Parameter Store keys (i.e., /medplum/demo/...)"),e.name=await f("What is your environment name?","demo"),r('Using environment name "'+e.name+'"...'),d("CONFIG FILE"),r("Medplum Infrastructure will create a config file in the current directory.");let o=await f("What is the config file name?",`medplum.${e.name}.config.json`);Ct(o)&&(r("Config file already exists."),await P("Do you want to overwrite the config file?")),r('Using config file "'+o+'"...'),p(o,e),d("AWS REGION"),r("Most Medplum resources will be created in a single AWS region."),e.region=await f("Enter your AWS region:","us-east-1"),p(o,e),d("AWS ACCOUNT NUMBER"),r("Medplum Infrastructure will use your AWS account number to create AWS resources."),t&&r("Using the AWS CLI, your current account ID is: "+t),e.accountNumber=await f("What is your AWS account number?",t),p(o,e),d("STACK NAME"),r("Medplum will create a CloudFormation stack to manage AWS resources."),r("AWS CloudFormation stack names ");let n="Medplum"+e.name.charAt(0).toUpperCase()+e.name.slice(1);for(e.stackName=await f("Enter your CloudFormation stack name?",n),p(o,e),d("BASE DOMAIN NAME"),r("Please enter the base domain name for your Medplum deployment."),r(""),r("Medplum deploys multiple subdomains for various services."),r(""),r('For example, "api." for the REST API and "app." for the web application.'),r("The base domain name is the common suffix for all subdomains."),r(""),r('For example, if your base domain name is "example.com",'),r('then the REST API will be "api.example.com".'),r(""),r('The base domain should include the TLD (i.e., ".com", ".org", ".net").'),r(""),r("Note that you must own the base domain, and it must use Route53 DNS.");!e.domainName;)e.domainName=await f("Enter your base domain name:");p(o,e),d("SUPPORT EMAIL"),r("Medplum sends transactional emails to users."),r("For example, emails to new users or for password reset."),r("Medplum will use the support email address to send these emails."),r("Note that you must verify the support email address in SES.");let a=await f("Enter your support email address:");d("API DOMAIN NAME"),r("Medplum deploys a REST API for the backend services."),e.apiDomainName=await f("Enter your REST API domain name:","api."+e.domainName),e.baseUrl=`https://${e.apiDomainName}/`,p(o,e),d("APP DOMAIN NAME"),r("Medplum deploys a web application for the user interface."),e.appDomainName=await f("Enter your web application domain name:","app."+e.domainName),p(o,e),d("STORAGE DOMAIN NAME"),r("Medplum deploys a storage service for file uploads."),e.storageDomainName=await f("Enter your storage domain name:","storage."+e.domainName),p(o,e),d("STORAGE BUCKET"),r("Medplum uses an S3 bucket to store binary content such as file uploads."),r("Medplum will create a the S3 bucket as part of the CloudFormation stack."),e.storageBucketName=await f("Enter your storage bucket name:","medplum-"+e.name+"-storage"),p(o,e),d("MAX AVAILABILITY ZONES"),r("Medplum API servers can be deployed in multiple availability zones."),r("This provides redundancy and high availability."),r("However, it also increases the cost of the deployment."),r("If you want to use all availability zones, choose a large number such as 99."),r("If you want to restrict the number, for example to manage EIP limits,"),r("then choose a small number such as 1 or 2."),e.maxAzs=await E("Enter the maximum number of availability zones:",[1,2,3,99],2),d("DATABASE INSTANCES"),r("Medplum uses a relational database to store data."),r("You can set up your own database,"),r("or Medplum can create a new RDS database as part of the CloudFormation stack."),await O("Do you want to create a new RDS database as part of the CloudFormation stack?")?(r("Medplum will create a new RDS database as part of the CloudFormation stack."),r(""),r("If you need high availability, you can choose multiple instances."),r("Use 1 for a single instance, or 2 for a primary and a standby."),e.rdsInstances=await E("Enter the number of database instances:",[1,2],1)):(r("Medplum will not create a new RDS database."),r("Please create a new RDS database and enter the database name, username, and password."),r('Set the AWS Secrets Manager secret ARN in the config file in the "rdsSecretsArn" setting.'),e.rdsSecretsArn="TODO"),p(o,e),d("SERVER INSTANCES"),r("Medplum uses AWS Fargate to run the API servers."),r("Medplum will create a new Fargate cluster as part of the CloudFormation stack."),r("Fargate will automatically scale the number of servers up and down."),r("If you need high availability, you can choose multiple instances."),e.desiredServerCount=await E("Enter the number of server instances:",[1,2,3,4,6,8],1),p(o,e),d("SERVER MEMORY"),r("You can choose the amount of memory for each server instance."),r("The default is 512 MB, which is sufficient for getting started."),r("Note that only certain CPU units are compatible with memory units."),r('Consult AWS Fargate "Task Definition Parameters" for more information.'),e.serverMemory=await E("Enter the server memory (MB):",[512,1024,2048,4096,8192,16384],512),p(o,e),d("SERVER CPU"),r("You can choose the amount of CPU for each server instance."),r("CPU is expressed as an integer using AWS CPU units"),r("The default is 256, which is sufficient for getting started."),r("Note that only certain CPU units are compatible with memory units."),r('Consult AWS Fargate "Task Definition Parameters" for more information.'),e.serverCpu=await E("Enter the server CPU:",[256,512,1024,2048,4096,8192,16384],256),p(o,e),d("SERVER IMAGE"),r("Medplum uses Docker images for the API servers."),r("You can choose the image to use for the servers."),r("Docker images can be loaded from either Docker Hub or AWS ECR."),r("The default is the latest Medplum release."),e.serverImage=await f("Enter the server image:","medplum/medplum-server:latest"),p(o,e),d("SIGNING KEY"),r("Medplum uses AWS CloudFront Presigned URLs for binary content such as file uploads.");let{privateKey:i,publicKey:s,passphrase:c}=At();e.storagePublicKey=s,p(o,e),d("SSL CERTIFICATES"),r("Medplum will now check for existing SSL certificates for the subdomains.");let u=await Pt(e.region);r("Found "+u.length+" certificate(s).");for(let{region:S,certName:w}of[{region:e.region,certName:"api"},{region:"us-east-1",certName:"app"},{region:"us-east-1",certName:"storage"}]){r("");let C=await xt(e,u,S,w);e[me(w)]=C,p(o,e)}d("AWS PARAMETER STORE"),r("Medplum uses AWS Parameter Store to store sensitive configuration values."),r("These values will be encrypted at rest."),r(`The values will be stored in the "/medplum/${e.name}" path.`);let g={port:e.apiPort,baseUrl:e.baseUrl,appBaseUrl:`https://${e.appDomainName}/`,storageBaseUrl:`https://${e.storageDomainName}/binary/`,binaryStorage:`s3:${e.storageBucketName}`,signingKey:i,signingKeyPassphrase:c,supportEmail:a};r(JSON.stringify({...g,signingKey:"****",signingKeyPassphrase:"****"},null,2)),await P("Do you want to store these values in AWS Parameter Store?"),await Rt(e.region,`/medplum/${e.name}/`,g),d("DONE!"),r("Medplum configuration complete."),r("You can now proceed to deploying the Medplum infrastructure with CDK."),r("Run:"),r(""),r(` npx cdk bootstrap -c config=${o}`),r(` npx cdk synth -c config=${o}`),e.region==="us-east-1"?r(` npx cdk deploy -c config=${o}`):r(` npx cdk deploy -c config=${o} --all`),r(""),r("See Medplum documentation for more information:"),r(""),r(" https://www.medplum.com/docs/self-hosting/install-on-aws"),r(""),x.close()}function r(e){x.write(e+`
|
|
3
3
|
`)}function d(e){r(`
|
|
4
4
|
`+e+`
|
|
5
|
-
`)}function f(e,t=""){return new Promise(o=>{P.question(e+(t?" ("+t+")":"")+" ",n=>{o(n||t.toString())})})}async function j(e,t,o=""){let n=e+" ["+t.map(a=>a===o?"("+a+")":a).join("|")+"]";for(;;){let a=await f(n)||o;if(t.includes(a))return a;r("Please choose one of the following options: "+t.join(", "))}}async function w(e,t,o){return parseInt(await j(e,t.map(n=>n.toString()),o.toString()),10)}async function $(e){return(await j(e,["y","n"])).toLowerCase()==="y"}async function k(e){if(!await $(e))throw r("Exiting..."),new Error("User cancelled")}function p(e,t){ft(gt(e),JSON.stringify(t,void 0,2),"utf-8")}async function St(e){try{let t=new lt({region:e}),o=new mt({});return(await t.send(o)).Account}catch(t){console.log("Warning: Unable to get AWS account ID",t.message);return}}async function wt(e){let t=await se(e);if(e!=="us-east-1"){let o=await se("us-east-1");t.push(...o)}return t}async function se(e){try{let t=new ce({region:e}),o=new rt({MaxItems:1e3});return(await t.send(o)).CertificateSummaryList}catch(t){return console.log("Warning: Unable to list certificates",t.message),[]}}async function Ct(e,t,o,n){let a=e[ht(n)],i=t.find(c=>c.CertificateArn?.includes(o)&&c.DomainName===a);if(i)return r(`Found existing certificate for "${a}" in "${o}.`),i.CertificateArn;if(r(`No existing certificate found for "${a}" in "${o}.`),!await $("Do you want to request a new certificate?"))return r(`Please add your certificate ARN to the config file in the "${me(n)}" setting.`),"TODO";let s=await vt(o,a);return r("Certificate ARN: "+s),s}async function vt(e,t){try{let o=await j("Validate certificate using DNS or email validation?",["dns","email"],"dns"),n=new ce({region:e}),a=new at({DomainName:t,ValidationMethod:o.toUpperCase()});return(await n.send(a)).CertificateArn}catch(o){return console.log("Error: Unable to request certificate",o.message),"TODO"}}function bt(){let e=ut(),t=dt("rsa",{modulusLength:2048,publicKeyEncoding:{type:"spki",format:"pem"},privateKeyEncoding:{type:"pkcs1",format:"pem",cipher:"aes-256-cbc",passphrase:e}});return{publicKey:t.publicKey,privateKey:t.privateKey,passphrase:e}}async function Et(e,t){let o=new it({Name:t,WithDecryption:!0});try{return(await e.send(o)).Parameter?.Value}catch(n){if(n.name==="ParameterNotFound")return;throw n}}async function Mt(e,t,o){let n=new st({Name:t,Value:o,Type:"SecureString",Overwrite:!0});await e.send(n)}async function kt(e,t,o){let n=new ct({region:e});for(let[a,i]of Object.entries(o)){let s=t+a,c=i.toString(),u=await Et(n,s);u!==void 0&&u!==c&&(r(`Parameter "${s}" exists with different value.`),await k(`Do you want to overwrite "${s}"?`)),await Mt(n,s,c)}}async function de(){let e=await B();for(let t of e){let o=t.StackName,n=await U(o);n&&(M(n),console.log(""))}}import{CreateInvalidationCommand as Tt}from"@aws-sdk/client-cloudfront";import{PutObjectCommand as Bt}from"@aws-sdk/client-s3";import Ut from"fast-glob";import{createReadStream as Ft,mkdtempSync as jt,readdirSync as $t,readFileSync as Lt,rmSync as Ot,writeFileSync as Wt}from"fs";import ge from"node-fetch";import{tmpdir as _t}from"os";import{join as A,sep as Kt}from"path";import{pipeline as qt}from"stream/promises";import{existsSync as Pt,readFileSync as xt,writeFile as At}from"fs";import{resolve as Dt}from"path";import Nt from"tar";function y(e){console.log(JSON.stringify(e,null,2))}async function L(e,t,o){let n=W(t.source);if(n)try{console.log("Update bot code.....");let a=await e.updateResource({...o,code:n});console.log(a?"Success! New bot version: "+a.meta?.versionId:"Bot not modified")}catch(a){console.log("Update error: ",a)}}async function ue(e,t,o){let n=W(t.dist??t.source);if(n)try{console.log("Deploying bot...");let a=await e.post(e.fhirUrl("Bot",o.id,"$deploy"),{code:n});console.log("Deploy result: "+a.issue?.[0]?.details?.text)}catch(a){console.log("Deploy error: ",a)}}async function O(e,t){if(t.length<4){console.log("Error: command needs to be npx medplum <new-bot-name> <project-id> <source-file> <dist-file>");return}let o=t[0],n=t[1],a=t[2],i=t[3];try{let s={name:o,description:""},c=await e.post("admin/projects/"+n+"/bot",s),u=await e.readResource("Bot",c.id),g={name:o,id:c.id,source:a,dist:i};await L(e,g,u),console.log(`Success! Bot created: ${u.id}`),It(g)}catch(s){console.log("Error while creating new bot: "+s)}}function pe(e){let t=new RegExp("^"+Rt(e).replace(/\\\*/g,".*")+"$"),o=x()?.bots?.filter(n=>t.test(n.name));return o||[]}function x(e){let t=e?`medplum.${e}.config.json`:"medplum.config.json",o=W(t);if(o)return JSON.parse(o)}function W(e){let t=Dt(process.cwd(),e);return Pt(t)?xt(t,"utf8"):(console.log("Error: File does not exist: "+t),"")}function It(e){let t=x();t?.bots?.push(e),At("medplum.config.json",JSON.stringify(t),()=>{console.log(`Bot added to config: ${e.id}`)})}function Rt(e){return e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")}function fe(e){let n=0,a=0;return Nt.x({cwd:e,filter:(i,s)=>{if(n++,n>100)throw new Error("Tar extractor reached max number of files");if(a+=s.size,a>10485760)throw new Error("Tar extractor reached max size");return!0}})}function _(){return{extension:[{url:"http://hl7.org/fhir/StructureDefinition/data-absent-reason",valueCode:"unsupported"}]}}async function ye(e){let t=x(e);if(!t){console.log("Config not found");return}let o=await S(e);if(!o){console.log("Stack not found");return}let n=o.appBucket;if(!n){console.log("App bucket not found");return}let a=await Gt("@medplum/app","latest");he(a,{MEDPLUM_BASE_URL:t.baseUrl,MEDPLUM_CLIENT_ID:t.clientId||"",GOOGLE_CLIENT_ID:t.googleClientId||"",RECAPTCHA_SITE_KEY:t.recaptchaSiteKey||"",MEDPLUM_REGISTER_ENABLED:t.registerEnabled?"true":"false"}),await Vt(a,n.PhysicalResourceId),o.appDistribution?.PhysicalResourceId&&await Xt(o.appDistribution.PhysicalResourceId),console.log("Done")}async function zt(e,t){let o=`https://registry.npmjs.org/${e}/${t}`;return(await ge(o)).json()}async function Gt(e,t){let n=(await zt(e,t)).dist.tarball,a=jt(A(_t(),"tarball-"));try{let i=await ge(n),s=fe(a);return await qt(i.body,s),A(a,"package","dist")}catch(i){throw Ot(a,{recursive:!0,force:!0}),i}}function he(e,t){for(let o of $t(e,{withFileTypes:!0})){let n=A(e,o.name);o.isDirectory()?he(n,t):o.isFile()&&n.endsWith(".js")&&Ht(n,t)}}function Ht(e,t){let o=Lt(e,"utf-8");for(let[n,a]of Object.entries(t))o=o.replaceAll(`__${n}__`,a);Wt(e,o)}async function Vt(e,t){let o=[["css/**/*.css","text/css",!0],["css/**/*.css.map","application/json",!0],["img/**/*.png","image/png",!0],["img/**/*.svg","image/svg+xml",!0],["js/**/*.js","application/javascript",!0],["js/**/*.js.map","application/json",!0],["js/**/*.txt","text/plain",!0],["favicon.ico","image/vnd.microsoft.icon",!0],["robots.txt","text/plain",!0],["workbox-*.js","application/javascript",!0],["workbox-*.js.map","application/json",!0],["manifest.webmanifest","application/manifest+json",!1],["service-worker.js","application/javascript",!1],["service-worker.js.map","application/json",!1],["index.html","text/html",!1]];for(let n of o)await Yt({rootDir:e,bucketName:t,fileNamePattern:n[0],contentType:n[1],cached:n[2]})}async function Yt(e){let t=Ut.sync(e.fileNamePattern,{cwd:e.rootDir});for(let o of t)await Jt(A(e.rootDir,o),e)}async function Jt(e,t){let o=Ft(e),n=e.substring(t.rootDir.length+1).split(Kt).join("/"),a={Bucket:t.bucketName,Key:n,Body:o,ContentType:t.contentType,CacheControl:t.cached?"public, max-age=31536000":"no-cache, no-store, must-revalidate"};console.log(`Uploading ${n} to ${t.bucketName}...`),await ae.send(new Bt(a))}async function Xt(e){let t=await ne.send(new Tt({DistributionId:e,InvalidationBatch:{CallerReference:`invalidate-all-${Date.now()}`,Paths:{Quantity:1,Items:["/*"]}}}));console.log(`Created invalidation with ID: ${t.Invalidation?.Id}`)}import{UpdateServiceCommand as Zt}from"@aws-sdk/client-ecs";async function Se(e){let t=await S(e);if(!t){console.log("Stack not found");return}let o=t.ecsCluster?.PhysicalResourceId;if(!o){console.log("ECS Cluster not found");return}let n=F(t.ecsService);if(!n){console.log("ECS Service not found");return}await re.send(new Zt({cluster:o,service:n,forceNewDeployment:!0})),console.log(`Service "${n}" updated successfully.`)}var h=new Qt("aws").description("Commands to manage AWS resources");h.command("init").description("Initialize a new Medplum AWS CloudFormation stacks").action(le);h.command("list").description("List Medplum AWS CloudFormation stacks").action(de);h.command("describe").description("Describe a Medplum AWS CloudFormation stack by tag").argument("<tag>").action(ie);h.command("update-server").alias("deploy-server").description("Update the server image").argument("<tag>").action(Se);h.command("update-app").alias("deploy-app").description("Update the app site").argument("<tag>").action(ye);import{Command as eo}from"commander";var we=m("save"),Ce=m("deploy"),ve=m("create"),be=new eo("bot").addCommand(we).addCommand(Ce).addCommand(ve),K=m("save-bot"),q=m("deploy-bot"),z=m("create-bot");we.description("Saving the bot").argument("<botName>").action(async(e,t)=>{let o=await l(t);await D(o,e)});Ce.description("Deploy the app to AWS").argument("<botName>").action(async(e,t)=>{let o=await l(t);await D(o,e,!0)});ve.arguments("<botName> <projectId> <sourceFile> <distFile>").description("Creating a bot").action(async(e,t,o,n,a)=>{let i=await l(a);await O(i,[e,t,o,n])});async function D(e,t,o=!1){let n=pe(t);for(let a of n){let i=await e.readResource("Bot",a.id);await L(e,a,i),o&&await ue(e,a,i)}console.log(`Number of bots deployed: ${n.length}`)}K.description("Saves the bot").argument("<botName>").action(async(e,t)=>{let o=await l(t);await D(o,e)});q.description("Deploy the bot to AWS").argument("<botName>").action(async(e,t)=>{let o=await l(t);await D(o,e,!0)});z.arguments("<botName> <projectId> <sourceFile> <distFile>").description("Creates and saves the bot").action(async(e,t,o,n,a)=>{let i=await l(a);await O(i,[e,t,o,n])});import{Command as to}from"commander";import{createReadStream as oo,writeFile as no}from"fs";import{resolve as Me}from"path";import{createInterface as ro}from"readline";var ke=m("export"),Pe=m("import"),xe=new to("bulk").addCommand(ke).addCommand(Pe);ke.option("-e, --export-level <exportLevel>",'Optional export level. Defaults to system level export. "Group/:id" - Group of Patients, "Patient" - All Patients.').option("-t, --types <types>","optional resource types to export").option("-s, --since <since>","optional Resources will be included in the response if their state has changed after the supplied time (e.g. if Resource.meta.lastUpdated is later than the supplied _since time).").option("-d, --target-directory <targetDirectory>","optional target directory to save files from the bulk export operations.").action(async e=>{let{exportLevel:t,types:o,since:n,targetDirectory:a}=e,i=await l(e);(await i.bulkExport(t,o,n)).output?.forEach(async({type:c,url:u})=>{let g=new URL(u),N=await i.download(u),v=`${c}_${g.pathname}`.replace(/[^a-zA-Z0-9]+/g,"_")+".ndjson",b=Me(a??"",v);no(`${b}`,await N.text(),()=>{console.log(`${b} is created`)})})});Pe.argument("<filename>","File Name").option("--num-resources-per-request <numResourcesPerRequest>","optional number of resources to import per batch request. Defaults to 25.","25").option("--add-extensions-for-missing-values","optional flag to add extensions for missing values in a resource",!1).option("-d, --target-directory <targetDirectory>","optional target directory of file to be imported").action(async(e,t)=>{let{numResourcesPerRequest:o,addExtensionsForMissingValues:n,targetDirectory:a}=t,i=Me(a??process.cwd(),e),s=await l(t);await ao(i,parseInt(o,10),s,n)});async function ao(e,t,o,n){let a=[],i=oo(e),s=ro({input:i});for await(let c of s){let u=io(c,n);a.push({resource:u,request:{method:"POST",url:u.resourceType}}),a.length%t===0&&(await Ee(a,o),a=[])}a.length>0&&await Ee(a,o)}async function Ee(e,t){(await t.executeBatch({resourceType:"Bundle",type:"transaction",entry:e})).entry?.forEach(n=>{y(n.response)})}function io(e,t){let o=JSON.parse(e);return t?so(o):o}function so(e){return e.resourceType==="ExplanationOfBenefit"?co(e):e}function co(e){return e.provider||(e.provider=_()),e.item?.forEach(t=>{t?.productOrService||(t.productOrService=_())}),e}import{Command as mo,Option as lo}from"commander";var Ae=m("list"),De=m("current"),Ne=m("switch"),Ie=m("invite"),Re=new mo("project").addCommand(Ae).addCommand(De).addCommand(Ne).addCommand(Ie);Ae.description("List of current projects").action(async e=>{let t=await l(e);uo(t)});function uo(e){let o=e.getLogins().map(n=>`${n.project.display} (${n.project.reference})`).join(`
|
|
5
|
+
`)}function f(e,t=""){return new Promise(o=>{x.question(e+(t?" ("+t+")":"")+" ",n=>{o(n||t.toString())})})}async function F(e,t,o=""){let n=e+" ["+t.map(a=>a===o?"("+a+")":a).join("|")+"]";for(;;){let a=await f(n)||o;if(t.includes(a))return a;r("Please choose one of the following options: "+t.join(", "))}}async function E(e,t,o){return parseInt(await F(e,t.map(n=>n.toString()),o.toString()),10)}async function O(e){return(await F(e,["y","n"])).toLowerCase()==="y"}async function P(e){if(!await O(e))throw r("Exiting..."),new Error("User cancelled")}function p(e,t){bt(vt(e),JSON.stringify(t,void 0,2),"utf-8")}async function kt(e){try{let t=new ht({region:e}),o=new yt({});return(await t.send(o)).Account}catch(t){console.log("Warning: Unable to get AWS account ID",t.message);return}}async function Pt(e){let t=await se(e);if(e!=="us-east-1"){let o=await se("us-east-1");t.push(...o)}return t}async function se(e){try{let t=new ce({region:e}),o=new dt({MaxItems:1e3});return(await t.send(o)).CertificateSummaryList}catch(t){return console.log("Warning: Unable to list certificates",t.message),[]}}async function xt(e,t,o,n){let a=e[Mt(n)],i=t.find(c=>c.CertificateArn?.includes(o)&&c.DomainName===a);if(i)return r(`Found existing certificate for "${a}" in "${o}.`),i.CertificateArn;if(r(`No existing certificate found for "${a}" in "${o}.`),!await O("Do you want to request a new certificate?"))return r(`Please add your certificate ARN to the config file in the "${me(n)}" setting.`),"TODO";let s=await Nt(o,a);return r("Certificate ARN: "+s),s}async function Nt(e,t){try{let o=await F("Validate certificate using DNS or email validation?",["dns","email"],"dns"),n=new ce({region:e}),a=new ut({DomainName:t,ValidationMethod:o.toUpperCase()});return(await n.send(a)).CertificateArn}catch(o){return console.log("Error: Unable to request certificate",o.message),"TODO"}}function At(){let e=wt(),t=St("rsa",{modulusLength:2048,publicKeyEncoding:{type:"spki",format:"pem"},privateKeyEncoding:{type:"pkcs1",format:"pem",cipher:"aes-256-cbc",passphrase:e}});return{publicKey:t.publicKey,privateKey:t.privateKey,passphrase:e}}async function Dt(e,t){let o=new pt({Name:t,WithDecryption:!0});try{return(await e.send(o)).Parameter?.Value}catch(n){if(n.name==="ParameterNotFound")return;throw n}}async function It(e,t,o){let n=new ft({Name:t,Value:o,Type:"SecureString",Overwrite:!0});await e.send(n)}async function Rt(e,t,o){let n=new gt({region:e});for(let[a,i]of Object.entries(o)){let s=t+a,c=i.toString(),u=await Dt(n,s);u!==void 0&&u!==c&&(r(`Parameter "${s}" exists with different value.`),await P(`Do you want to overwrite "${s}"?`)),await It(n,s,c)}}async function de(){let e=await U();for(let t of e){let o=t.StackName,n=await B(o);n&&(k(n),console.log(""))}}import{CreateInvalidationCommand as Lt}from"@aws-sdk/client-cloudfront";import{PutObjectCommand as Wt}from"@aws-sdk/client-s3";import _t from"fast-glob";import{createReadStream as Kt,mkdtempSync as qt,readdirSync as zt,readFileSync as Gt,rmSync as Ht,writeFileSync as Vt}from"fs";import ge from"node-fetch";import{tmpdir as Jt}from"os";import{join as A,sep as Yt}from"path";import{pipeline as Xt}from"stream/promises";import{existsSync as Tt,readFileSync as Ut,writeFile as Bt}from"fs";import{resolve as jt}from"path";import Ft from"tar";function h(e){console.log(JSON.stringify(e,null,2))}async function $(e,t,o){let n=W(t.source);if(n)try{console.log("Update bot code.....");let a=await e.updateResource({...o,code:n});console.log(a?"Success! New bot version: "+a.meta?.versionId:"Bot not modified")}catch(a){console.log("Update error: ",a)}}async function ue(e,t,o){let n=W(t.dist??t.source);if(n)try{console.log("Deploying bot...");let a=await e.post(e.fhirUrl("Bot",o.id,"$deploy"),{code:n});console.log("Deploy result: "+a.issue?.[0]?.details?.text)}catch(a){console.log("Deploy error: ",a)}}async function L(e,t){if(t.length<4){console.log("Error: command needs to be npx medplum <new-bot-name> <project-id> <source-file> <dist-file>");return}let o=t[0],n=t[1],a=t[2],i=t[3];try{let s={name:o,description:""},c=await e.post("admin/projects/"+n+"/bot",s),u=await e.readResource("Bot",c.id),g={name:o,id:c.id,source:a,dist:i};await $(e,g,u),console.log(`Success! Bot created: ${u.id}`),Ot(g)}catch(s){console.log("Error while creating new bot: "+s)}}function pe(e){let t=new RegExp("^"+$t(e).replace(/\\\*/g,".*")+"$"),o=N()?.bots?.filter(n=>t.test(n.name));return o||[]}function N(e){let t=e?`medplum.${e}.config.json`:"medplum.config.json",o=W(t);if(o)return JSON.parse(o)}function W(e){let t=jt(process.cwd(),e);return Tt(t)?Ut(t,"utf8"):(console.log("Error: File does not exist: "+t),"")}function Ot(e){let t=N();t?.bots?.push(e),Bt("medplum.config.json",JSON.stringify(t),()=>{console.log(`Bot added to config: ${e.id}`)})}function $t(e){return e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")}function fe(e){let n=0,a=0;return Ft.x({cwd:e,filter:(i,s)=>{if(n++,n>100)throw new Error("Tar extractor reached max number of files");if(a+=s.size,a>10485760)throw new Error("Tar extractor reached max size");return!0}})}function _(){return{extension:[{url:"http://hl7.org/fhir/StructureDefinition/data-absent-reason",valueCode:"unsupported"}]}}async function ye(e){let t=N(e);if(!t){console.log("Config not found");return}let o=await v(e);if(!o){console.log("Stack not found");return}let n=o.appBucket;if(!n){console.log("App bucket not found");return}let a=await Qt("@medplum/app","latest");he(a,{MEDPLUM_BASE_URL:t.baseUrl,MEDPLUM_CLIENT_ID:t.clientId??"",GOOGLE_CLIENT_ID:t.googleClientId??"",RECAPTCHA_SITE_KEY:t.recaptchaSiteKey??"",MEDPLUM_REGISTER_ENABLED:t.registerEnabled?"true":"false"}),await to(a,n.PhysicalResourceId),o.appDistribution?.PhysicalResourceId&&await ro(o.appDistribution.PhysicalResourceId),console.log("Done")}async function Zt(e,t){let o=`https://registry.npmjs.org/${e}/${t}`;return(await ge(o)).json()}async function Qt(e,t){let n=(await Zt(e,t)).dist.tarball,a=qt(A(Jt(),"tarball-"));try{let i=await ge(n),s=fe(a);return await Xt(i.body,s),A(a,"package","dist")}catch(i){throw Ht(a,{recursive:!0,force:!0}),i}}function he(e,t){for(let o of zt(e,{withFileTypes:!0})){let n=A(e,o.name);o.isDirectory()?he(n,t):o.isFile()&&n.endsWith(".js")&&eo(n,t)}}function eo(e,t){let o=Gt(e,"utf-8");for(let[n,a]of Object.entries(t))o=o.replaceAll(`__${n}__`,a);Vt(e,o)}async function to(e,t){let o=[["css/**/*.css","text/css",!0],["css/**/*.css.map","application/json",!0],["img/**/*.png","image/png",!0],["img/**/*.svg","image/svg+xml",!0],["js/**/*.js","application/javascript",!0],["js/**/*.js.map","application/json",!0],["js/**/*.txt","text/plain",!0],["favicon.ico","image/vnd.microsoft.icon",!0],["robots.txt","text/plain",!0],["workbox-*.js","application/javascript",!0],["workbox-*.js.map","application/json",!0],["manifest.webmanifest","application/manifest+json",!1],["service-worker.js","application/javascript",!1],["service-worker.js.map","application/json",!1],["index.html","text/html",!1]];for(let n of o)await oo({rootDir:e,bucketName:t,fileNamePattern:n[0],contentType:n[1],cached:n[2]})}async function oo(e){let t=_t.sync(e.fileNamePattern,{cwd:e.rootDir});for(let o of t)await no(A(e.rootDir,o),e)}async function no(e,t){let o=Kt(e),n=e.substring(t.rootDir.length+1).split(Yt).join("/"),a={Bucket:t.bucketName,Key:n,Body:o,ContentType:t.contentType,CacheControl:t.cached?"public, max-age=31536000":"no-cache, no-store, must-revalidate"};console.log(`Uploading ${n} to ${t.bucketName}...`),await ae.send(new Wt(a))}async function ro(e){let t=await ne.send(new Lt({DistributionId:e,InvalidationBatch:{CallerReference:`invalidate-all-${Date.now()}`,Paths:{Quantity:1,Items:["/*"]}}}));console.log(`Created invalidation with ID: ${t.Invalidation?.Id}`)}import{UpdateServiceCommand as ao}from"@aws-sdk/client-ecs";async function Se(e){let t=await v(e);if(!t){console.log("Stack not found");return}let o=t.ecsCluster?.PhysicalResourceId;if(!o){console.log("ECS Cluster not found");return}let n=j(t.ecsService);if(!n){console.log("ECS Service not found");return}await re.send(new ao({cluster:o,service:n,forceNewDeployment:!0})),console.log(`Service "${n}" updated successfully.`)}var b=new io("aws").description("Commands to manage AWS resources");b.command("init").description("Initialize a new Medplum AWS CloudFormation stacks").action(le);b.command("list").description("List Medplum AWS CloudFormation stacks").action(de);b.command("describe").description("Describe a Medplum AWS CloudFormation stack by tag").argument("<tag>").action(ie);b.command("update-server").alias("deploy-server").description("Update the server image").argument("<tag>").action(Se);b.command("update-app").alias("deploy-app").description("Update the app site").argument("<tag>").action(ye);import{Command as so}from"commander";var we=m("save"),Ce=m("deploy"),be=m("create"),ve=new so("bot").addCommand(we).addCommand(Ce).addCommand(be),K=m("save-bot"),q=m("deploy-bot"),z=m("create-bot");we.description("Saving the bot").argument("<botName>").action(async(e,t)=>{let o=await l(t);await D(o,e)});Ce.description("Deploy the app to AWS").argument("<botName>").action(async(e,t)=>{let o=await l(t);await D(o,e,!0)});be.arguments("<botName> <projectId> <sourceFile> <distFile>").description("Creating a bot").action(async(e,t,o,n,a)=>{let i=await l(a);await L(i,[e,t,o,n])});async function D(e,t,o=!1){let n=pe(t);for(let a of n){let i=await e.readResource("Bot",a.id);await $(e,a,i),o&&await ue(e,a,i)}console.log(`Number of bots deployed: ${n.length}`)}K.description("Saves the bot").argument("<botName>").action(async(e,t)=>{let o=await l(t);await D(o,e)});q.description("Deploy the bot to AWS").argument("<botName>").action(async(e,t)=>{let o=await l(t);await D(o,e,!0)});z.arguments("<botName> <projectId> <sourceFile> <distFile>").description("Creates and saves the bot").action(async(e,t,o,n,a)=>{let i=await l(a);await L(i,[e,t,o,n])});import{Command as co}from"commander";import{createReadStream as mo,writeFile as lo}from"fs";import{resolve as Me}from"path";import{createInterface as uo}from"readline";var ke=m("export"),Pe=m("import"),xe=new co("bulk").addCommand(ke).addCommand(Pe);ke.option("-e, --export-level <exportLevel>",'Optional export level. Defaults to system level export. "Group/:id" - Group of Patients, "Patient" - All Patients.').option("-t, --types <types>","optional resource types to export").option("-s, --since <since>","optional Resources will be included in the response if their state has changed after the supplied time (e.g. if Resource.meta.lastUpdated is later than the supplied _since time).").option("-d, --target-directory <targetDirectory>","optional target directory to save files from the bulk export operations.").action(async e=>{let{exportLevel:t,types:o,since:n,targetDirectory:a}=e,i=await l(e);(await i.bulkExport(t,o,n)).output?.forEach(async({type:c,url:u})=>{let g=new URL(u),S=await i.download(u),w=`${c}_${g.pathname}`.replace(/[^a-zA-Z0-9]+/g,"_")+".ndjson",C=Me(a??"",w);lo(`${C}`,await S.text(),()=>{console.log(`${C} is created`)})})});Pe.argument("<filename>","File Name").option("--num-resources-per-request <numResourcesPerRequest>","optional number of resources to import per batch request. Defaults to 25.","25").option("--add-extensions-for-missing-values","optional flag to add extensions for missing values in a resource",!1).option("-d, --target-directory <targetDirectory>","optional target directory of file to be imported").action(async(e,t)=>{let{numResourcesPerRequest:o,addExtensionsForMissingValues:n,targetDirectory:a}=t,i=Me(a??process.cwd(),e),s=await l(t);await po(i,parseInt(o,10),s,n)});async function po(e,t,o,n){let a=[],i=mo(e),s=uo({input:i});for await(let c of s){let u=fo(c,n);a.push({resource:u,request:{method:"POST",url:u.resourceType}}),a.length%t===0&&(await Ee(a,o),a=[])}a.length>0&&await Ee(a,o)}async function Ee(e,t){(await t.executeBatch({resourceType:"Bundle",type:"transaction",entry:e})).entry?.forEach(n=>{h(n.response)})}function fo(e,t){let o=JSON.parse(e);return t?go(o):o}function go(e){return e.resourceType==="ExplanationOfBenefit"?yo(e):e}function yo(e){return e.provider||(e.provider=_()),e.item?.forEach(t=>{t?.productOrService||(t.productOrService=_())}),e}import{Command as ho,Option as So}from"commander";var Ne=m("list"),Ae=m("current"),De=m("switch"),Ie=m("invite"),Re=new ho("project").addCommand(Ne).addCommand(Ae).addCommand(De).addCommand(Ie);Ne.description("List of current projects").action(async e=>{let t=await l(e);wo(t)});function wo(e){let o=e.getLogins().map(n=>`${n.project.display} (${n.project.reference})`).join(`
|
|
6
6
|
|
|
7
|
-
`);console.log(o)}
|
|
8
|
-
`)):console.log(`Error: project ${t} not found. Make sure you are added as a user to this project`)}async function
|
|
7
|
+
`);console.log(o)}Ae.description("Project you are currently on").action(async e=>{let o=(await l(e)).getActiveLogin();if(!o)throw new Error("Unauthenticated: run `npx medplum login` to login");console.log(`${o.project.display} (${o.project.reference})`)});De.description("Switching to another project from the current one").argument("<projectId>").action(async(e,t)=>{let o=await l(t);await Co(o,e)});Ie.description("Invite a member to your current project (run npx medplum project current to confirm)").arguments("<firstName> <lastName> <email>").option("--send-email","If you want to send the email when inviting the user").option("--admin","If the user you are inviting is an admin").addOption(new So("-r, --role <role>","Role of user").choices(["Practitioner","Patient","RelatedPerson"]).default("Practitioner")).action(async(e,t,o,n)=>{let a=await l(n),i=a.getActiveLogin();if(!i)throw new Error("Unauthenticated: run `npx medplum login` to login");if(!i.project.reference)throw new Error("No current project to invite user to");let s=i.project.reference.split("/")[1],c={resourceType:n.role,firstName:e,lastName:t,email:o,sendEmail:!!n.sendEmail,admin:!!n.admin};await bo(s,c,a)});async function Co(e,t){let n=e.getLogins().find(a=>a.project.reference?.includes(t));n?(await e.setActiveLogin(n),console.log(`Switched to project ${t}
|
|
8
|
+
`)):console.log(`Error: project ${t} not found. Make sure you are added as a user to this project`)}async function bo(e,t,o){try{await o.invite(e,t),t.sendEmail&&console.log("Email sent"),console.log("See your users at https://app.medplum.com/admin/users")}catch(n){console.log("Error while sending invite "+n)}}import{convertToTransactionBundle as vo}from"@medplum/core";var G=m("delete"),H=m("get"),V=m("patch"),J=m("post"),Y=m("put");G.argument("<url>","Resource/$id").action(async(e,t)=>{let o=await l(t);h(await o.delete(M(e,t)))});H.argument("<url>","Resource/$id").option("--as-transaction","Print out the bundle as a transaction type").action(async(e,t)=>{let n=await(await l(t)).get(M(e,t));t.asTransaction?h(vo(n)):h(n)});V.arguments("<url> <body>").action(async(e,t,o)=>{let n=await l(o);h(await n.patch(M(e,o),X(t)))});J.arguments("<url> <body>").action(async(e,t,o)=>{let n=await l(o);h(await n.post(M(e,o),X(t)))});Y.arguments("<url> <body>").action(async(e,t,o)=>{let n=await l(o);h(await n.put(M(e,o),X(t)))});function X(e){if(e)try{return JSON.parse(e)}catch{return e}}function M(e,t){let o=["admin/","auth/","fhir/R4"],{fhirUrlPath:n}=t;return o.some(a=>e.startsWith(a))?e:n?`${n}/${e}`:"fhir/R4/"+e}import{Command as Eo}from"commander";import{resolve as Mo}from"path";import{readdirSync as ko}from"fs";import{homedir as Po}from"os";var Te=m("set"),Ue=m("remove"),Be=m("list"),je=m("describe"),Fe=new Eo("profile").addCommand(Te).addCommand(Ue).addCommand(Be).addCommand(je);Te.argument("<profileName>","Name of the profile").description("Create a new profile or replace it with the given name and its associated properties").action(async(e,t)=>{new y(e).setObject("options",t),console.log(`${e} profile created`)});Ue.argument("<profileName>","Name of the profile").description("Remove a profile by name").action(async e=>{new y(e).setObject("options",void 0),console.log(`${e} profile removed`)});Be.description("List all profiles saved").action(async()=>{let e=Mo(Po(),".medplum"),t=ko(e),o=[];t.forEach(n=>{let a=n.split(".")[0],s=new y(a).getObject("options");s&&o.push({profileName:a,profile:s})}),console.log(o)});je.argument("<profileName>","Name of the profile").description("Describes a profile").action(async e=>{let o=new y(e).getObject("options");console.log(o)});async function Io(e){try{let t=new Ao("medplum").description("Command to access Medplum CLI");t.version(xo),t.addCommand(I),t.addCommand(R),t.addCommand(H),t.addCommand(J),t.addCommand(V),t.addCommand(Y),t.addCommand(G),t.addCommand(Re),t.addCommand(xe),t.addCommand(ve),t.addCommand(K),t.addCommand(q),t.addCommand(z),t.addCommand(Fe),t.addCommand(b),await t.parseAsync(e)}catch(t){console.error("Error: "+No(t))}}async function Ro(){Do.config(),await Io(process.argv)}Z.main===module&&Ro().catch(e=>console.error("Unhandled error:",e));export{Io as main,Ro as run};
|
|
9
9
|
//# sourceMappingURL=index.mjs.map
|