From 703910cfc7fcac2e8f0ff2b6237545daf7712b71 Mon Sep 17 00:00:00 2001 From: Vladimir Ignatov Date: Tue, 28 May 2024 11:21:32 -0400 Subject: [PATCH] Proposed CDS Hooks launch support --- backend/index.ts | 3 +- backend/routes/auth/authorize.ts | 6 +-- backend/routes/fhir/index.ts | 21 +++++++++- index.d.ts | 3 +- src/components/Launcher/index.tsx | 68 ++++++++++++++++++++++--------- src/isomorphic/codec.ts | 3 +- 6 files changed, 77 insertions(+), 27 deletions(-) diff --git a/backend/index.ts b/backend/index.ts index e3dc012..50ec6b2 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -75,7 +75,8 @@ app.use("/env.js", (_, res) => { FHIR_SERVER_R4 : config.fhirServerR4, ACCESS_TOKEN : jwt.sign({ client_id: "launcherUI" }, config.jwtSecret, { expiresIn: "10 years" }), VERSION : pkg.version, - COMMIT : process.env.SOURCE_VERSION + COMMIT : process.env.SOURCE_VERSION, + CDS_SANDBOX_URL : process.env.CDS_SANDBOX_URL }; res.type("application/javascript").send(`var ENV = ${JSON.stringify(out, null, 4)};`); diff --git a/backend/routes/auth/authorize.ts b/backend/routes/auth/authorize.ts index 272e2c3..7290087 100644 --- a/backend/routes/auth/authorize.ts +++ b/backend/routes/auth/authorize.ts @@ -243,9 +243,9 @@ export default class AuthorizeHandler { return scope.has("launch/patient") || scope.has("launch"); } - // if (launch_type === "cds-hooks") { - // return scope.has("launch/patient") || scope.has("launch"); - // } + if (launch_type === "cds-hooks") { + return scope.has("launch/patient") || scope.has("launch"); + } return false } diff --git a/backend/routes/fhir/index.ts b/backend/routes/fhir/index.ts index 7cebf28..9a6f27a 100644 --- a/backend/routes/fhir/index.ts +++ b/backend/routes/fhir/index.ts @@ -1,4 +1,4 @@ -import { Router, text } from "express" +import { Router, json, text } from "express" import getWellKnownSmartConfig from "./.well-known/smart-configuration" import getWellKnownOpenidConfig from "./.well-known/openid-configuration" import getCapabilityStatement from "./metadata" @@ -11,6 +11,25 @@ const router = Router({ mergeParams: true }) router.get("/.well-known/smart-configuration" , getWellKnownSmartConfig) router.get("/.well-known/openid-configuration", getWellKnownOpenidConfig) router.get("/metadata", asyncRouteWrap(getCapabilityStatement)) + + +// Provide launch_id if the CDS Sandbox asks for it +router.post("/_services/smart/Launch", json(), (req, res) => { + // { + // "launchUrl":"https://examples.smarthealthit.org/growth-chart-app/launch.html", + // "parameters":{ + // "patient":"2e27c71e-30c8-4ceb-8c1c-5641e066c0a4", + // "smart_messaging_origin":"https://sandbox.cds-hooks.org", + // "appContext":"{\"patient\":\"099e7de7-c952-40e2-9b4e-0face78c9d80\",\"encounter\": \"1d3f33a3-5e0b-4508-8836-ecabcab2ff4c\"}" + // } + // } + res.json({ + launch_id: Buffer.from(JSON.stringify({ + context: req.body.parameters || {} + }), "utf8").toString("base64") + }); +}); + router.use("/", text({ type: "*/*", limit: 1e6 }), asyncRouteWrap(fhirProxy)) export default router diff --git a/index.d.ts b/index.d.ts index 72e4fc0..4d22e8c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -11,6 +11,7 @@ declare global { ACCESS_TOKEN : string VERSION : string COMMIT : string + CDS_SANDBOX_URL : string } } @@ -19,7 +20,7 @@ declare namespace SMART { /** * All the launch types that we recognize */ - type LaunchType = "provider-ehr" | "patient-portal" | "provider-standalone" | "patient-standalone" | "backend-service"; //| "cds-hooks"; + type LaunchType = "provider-ehr" | "patient-portal" | "provider-standalone" | "patient-standalone" | "backend-service" | "cds-hooks"; type SimulatedError = diff --git a/src/components/Launcher/index.tsx b/src/components/Launcher/index.tsx index b2531d3..15da9ce 100644 --- a/src/components/Launcher/index.tsx +++ b/src/components/Launcher/index.tsx @@ -37,6 +37,14 @@ const launchTypes = [ } ]; +if (window.ENV.CDS_SANDBOX_URL) { + launchTypes.push({ + name : "CDS Hooks Service", + description: "Test your CDS services", + value : "cds-hooks" + }) +} + const DEFAULT_LAUNCH_PARAMS: SMART.LaunchParams = { launch_type : "provider-ehr", patient : "", @@ -122,6 +130,7 @@ export default function Launcher() { const isStandaloneLaunch = launch_type.includes("standalone"); const isBackendService = launch_type === "backend-service"; + const isCDSHooksLaunch = launch_type === "cds-hooks"; const { origin } = window.location; @@ -172,8 +181,14 @@ export default function Launcher() { userLaunchUrl = new URL(`/ehr?app=${encodeURIComponent(userLaunchUrl.href)}`, origin); } + if (isCDSHooksLaunch) { + userLaunchUrl = new URL("/launch.html", ENV.CDS_SANDBOX_URL) + userLaunchUrl.searchParams.set("launch", launchCode); + userLaunchUrl.searchParams.set("iss", iss); + } + let validationErrors = getValidationErrors(launch, query); - + return ( @@ -190,9 +205,9 @@ export default function Launcher() {
  • setQuery({ tab: "0" }) }> App Launch Options
  • -
  • setQuery({ tab: "1" }) }> + { !isCDSHooksLaunch &&
  • setQuery({ tab: "1" }) }> Client Registration & Validation -
  • + }
    e.preventDefault()}>
    @@ -208,7 +223,11 @@ export default function Launcher() {

    { - isStandaloneLaunch || launch_type === "backend-service" ? "Server's FHIR Base URL" : "App's Launch URL" + isStandaloneLaunch || launch_type === "backend-service" ? + "Server's FHIR Base URL" : + launch_type === "cds-hooks" ? + "Discovery Endpoint URL" : + "App's Launch URL" }

    @@ -221,9 +240,15 @@ export default function Launcher() { value={ isStandaloneLaunch || isBackendService ? aud : launch_url } onChange={ e => !isStandaloneLaunch && !isBackendService && setQuery({ launch_url: e.target.value }) } readOnly={ isStandaloneLaunch || isBackendService } + placeholder={ isStandaloneLaunch || isBackendService ? + undefined : + launch_type === "cds-hooks" ? + "Discovery Endpoint URL" : + "Launch URL" + } /> - { isStandaloneLaunch || isBackendService ? + { (isStandaloneLaunch || isBackendService) ? : validationErrors.length ? : @@ -238,19 +263,24 @@ export default function Launcher() {
    -
    + { launch_type !== "cds-hooks" &&
    { validationErrors.filter(e => e !== "Missing app launch URL" && e !== "Invalid app launch URL").length ? : Launch Sample App } -
    +
    }
    - { isStandaloneLaunch || launch_type === "backend-service" ? + { (isStandaloneLaunch || launch_type === "backend-service") ? Your app should use this url to connect to the sandbox FHIR server : + launch_type === "cds-hooks" ? + + If you have developed CDS service(s) enter your discovery endpoint + URL and click "Launch" to launch the CDS Hooks Sandbox. + : Full url of the page in your app that will initialize the SMART session (often the path to a launch.html file or endpoint) @@ -317,7 +347,7 @@ function LaunchTab() {
    -
    +
    -
    +
    }
    - { launch.launch_type !== "backend-service" && + { launch.launch_type !== "backend-service" && launch.launch_type !== "cds-hooks" &&
    @@ -444,10 +474,9 @@ function LaunchTab() { }} /> - Simulates the active patient in EHR when app is launched. If - no Patient ID is entered or if multiple comma delimited IDs - are specified, a patient picker will be displayed as part of - the launch flow. + Simulates the active patient in EHR when { launch.launch_type === "cds-hooks" ? "the CDS sandbox" : "app" } is + launched. If no Patient ID is entered or if multiple comma delimited IDs are specified, a patient picker will + be displayed as part of the launch flow.
    @@ -466,10 +495,9 @@ function LaunchTab() { }} /> - Simulates user who is launching the app. If no provider is - selected, or if multiple comma delimited Practitioner IDs - are specified, a login screen will be displayed as part of - the launch flow. + Simulates user who is launching the { launch.launch_type === "cds-hooks" ? "CDS sandbox" : "app" }. + If no provider is selected, or if multiple comma delimited Practitioner IDs are specified, + a login screen will be displayed as part of the launch flow.
    )} diff --git a/src/isomorphic/codec.ts b/src/isomorphic/codec.ts index 9e67a4d..542da24 100644 --- a/src/isomorphic/codec.ts +++ b/src/isomorphic/codec.ts @@ -19,7 +19,8 @@ export const launchTypes: SMART.LaunchType[] = [ "patient-portal", "provider-standalone", "patient-standalone", - "backend-service" + "backend-service", + "cds-hooks" ]; export const clientTypes: SMART.SMARTClientType[] = [