├── .npmrc ├── src ├── routes │ ├── +layout.ts │ └── +page.svelte ├── types.ts ├── util.ts ├── app.d.ts ├── config.ts ├── app.html ├── issuer.private.jwks.json ├── HealthLink.svelte ├── managementClient.ts └── AddFile.svelte ├── static ├── favicon.png └── ips │ ├── assets │ └── css │ │ └── custom.css │ ├── templates │ ├── Text.html │ ├── Composition.html │ ├── Patient.html │ ├── Immunizations.html │ ├── Problems.html │ ├── Allergies.html │ ├── Observations.html │ └── Medications.html │ └── index.html ├── vite.config.ts ├── .gitignore ├── .prettierignore ├── .prettierrc ├── tsconfig.json ├── svelte.config.js ├── package.json ├── .github └── workflows │ └── deploy.yml └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jddamore/shlips/main/static/favicon.png -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Bundle = unknown; 2 | export interface SHCRetrieveEvent { 3 | shc: SHCFile; 4 | patient: string; 5 | content: Bundle; 6 | } 7 | 8 | export interface SHCFile { 9 | "verifiableCredential": string[] 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as jose from 'jose'; 2 | 3 | export const base64url = jose.base64url; 4 | 5 | export function randomStringWithEntropy(entropy = 32): string { 6 | const b = new Uint8Array(entropy); 7 | crypto.getRandomValues(b); 8 | return base64url.encode(b); 9 | } 10 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { browser, dev} from '$app/environment'; 2 | 3 | export const API_BASE = 'https://api.vaxx.link/api'; 4 | export const VIEWER_BASE = (browser && !dev) ? new URL("ips#", window.location.href).toString(): 'http://localhost:5173/ips/index.html#' ; 5 | export const EXAMPLE_IPS = 'https://ips.health/fhir/Patient/98549f1a-e0d5-4454-849c-f5b97d3ed299/$summary' -------------------------------------------------------------------------------- /static/ips/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | .card { 2 | margin-bottom: 15px; 3 | } 4 | 5 | .icon-action { 6 | margin-top: 5px; 7 | float: right; 8 | font-size: 80%; 9 | } 10 | 11 | .list-group-item .title { 12 | margin-top: 5px; 13 | margin-bottom: 12px; 14 | font-weight: 600; 15 | } 16 | 17 | .textBody table { 18 | table-layout: fixed; 19 | width: 100%; 20 | 21 | } -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/issuer.private.jwks.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "crv": "P-256", 9 | "x": "11XvRWy1I2S0EyJlyf_bWfw_TQ5CJJNLw78bHXNxcgw", 10 | "y": "eZXwxvO1hvCY0KucrPfKo7yAyMT6Ajc3N7OkAB6VYy8", 11 | "d": "FvOOk6hMixJ2o9zt4PCfan_UW7i4aOEnzj76ZaCI9Og" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /static/ips/templates/Text.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {{titulo}} 7 |
8 |
9 | {{div}} 10 |
11 |
-------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /static/ips/templates/Composition.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Header 7 |
8 |
9 |
{{title}}
10 |

11 | Summary Date: {{date}} 12 |
13 | 14 |

15 |
16 |
-------------------------------------------------------------------------------- /static/ips/templates/Patient.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Patient 7 |
8 |
9 |
10 |

11 | Birth Date: {{birthDate}} 12 |
13 | Name: {{name[0].given}}, {{name[0].family}} 14 |

15 |
16 |
-------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/kit/vite'; 2 | import adapter from '@sveltejs/adapter-static'; 3 | 4 | const dev = process.argv.includes('dev'); 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | export default { 8 | preprocess: vitePreprocess(), 9 | kit: { 10 | adapter: adapter({ 11 | // default options are shown. On some platforms 12 | // these options are set automatically — see below 13 | pages: 'build', 14 | assets: 'build', 15 | fallback: null, 16 | precompress: false, 17 | strict: true, 18 | paths: { 19 | base: dev ? '' : '/shlips' 20 | } 21 | }) 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /static/ips/templates/Immunizations.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Immunizations 7 |
8 |
9 | 19 |
20 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shlips", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --plugin-search-dir . --check .", 12 | "format": "prettier --plugin-search-dir . --write ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "^2.0.0", 16 | "@sveltejs/adapter-static": "^2.0.0", 17 | "@sveltejs/kit": "^1.5.0", 18 | "prettier": "^2.8.0", 19 | "prettier-plugin-svelte": "^2.8.1", 20 | "svelte": "^3.54.0", 21 | "svelte-check": "^3.0.1", 22 | "tslib": "^2.4.1", 23 | "typescript": "^4.9.3", 24 | "vite": "^4.0.0" 25 | }, 26 | "type": "module", 27 | "dependencies": { 28 | "@types/pako": "^2.0.0", 29 | "@types/qrcode": "^1.5.0", 30 | "base64url": "^3.0.1", 31 | "jose": "^4.11.4", 32 | "pako": "^2.1.0", 33 | "qrcode": "^1.5.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: [ main ] 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out your repository using git 17 | uses: actions/checkout@v2 18 | 19 | - name: Use Node.js 18 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: '18' 23 | cache: 'npm' 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Build SvelteKit 29 | run: npm run build 30 | 31 | - name: Upload artifact 32 | uses: actions/upload-pages-artifact@v1 33 | with: 34 | path: ./build 35 | 36 | deploy: 37 | environment: 38 | name: github-pages 39 | url: ${{ steps.deployment.outputs.page_url }} 40 | runs-on: ubuntu-latest 41 | needs: build 42 | steps: 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@v1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /static/ips/templates/Problems.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Active problems / Diagnosis 7 |
8 |
9 | 24 |
25 |
-------------------------------------------------------------------------------- /static/ips/templates/Allergies.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Allergies and Intolerancies 7 |
8 |
9 | 29 |
30 |
-------------------------------------------------------------------------------- /src/HealthLink.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |
34 | 71 |
72 | 73 | 78 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 |

SMART Health Links for International Patient Summary

25 | 26 | {#if shl} 27 | 28 | 33 | {:else} 34 | { 36 | shl = await newShlFromShc(detail); 37 | }} 38 | /> 39 | {/if} 40 | 52 |
53 | 54 | 66 | -------------------------------------------------------------------------------- /static/ips/templates/Observations.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Observations 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{each(options.observations)}} 19 | 20 | 29 | 34 | 44 | {{if(options.observations[@index].category && options.observations[@index].category[0] && options.observations[@index].category[0].coding && options.observations[@index].category[0].coding[0])}} 45 | 48 | {{/if}} 49 | 50 | {{/each}} 51 |
NameDateValueCategory
21 | {{if(options.observations[@index].code && options.observations[@index].code.coding)}} 22 | {{@this.code.coding[0].display}} 23 | ({{@this.code.coding[0].code}}) 24 | {{/if}} 25 | {{if(options.observations[@index].code.text)}} 26 | [Uncoded text shown]: {{@this.code.text}} 27 | {{/if}} 28 | 30 | {{if(options.observations[@index].effectiveDateTime)}} 31 | {{@this.effectiveDateTime}} 32 | {{/if}} 33 | 35 | {{if(options.observations[@index].valueCodeableConcept)}} 36 | {{@this.valueCodeableConcept.coding[0].display}} 37 | {{/if}} 38 | {{if(options.observations[@index].valueQuantity)}} 39 | {{@this.valueQuantity.value}} 40 | {{@this.valueQuantity.code}} 41 | {{/if}} 42 | 43 | 46 | {{@this.category[0].coding[0].code}} 47 |
52 |
53 |
-------------------------------------------------------------------------------- /src/managementClient.ts: -------------------------------------------------------------------------------- 1 | import { randomStringWithEntropy, base64url } from './util'; 2 | import { API_BASE, VIEWER_BASE } from './config'; 3 | import * as jose from 'jose'; 4 | 5 | type ConfigForServer = Pick; 6 | 7 | export interface SHLAdminParams { 8 | id: string; 9 | managementToken: string; 10 | encryptionKey: string; 11 | files: { 12 | contentEncrypted: string; 13 | contentType: string; 14 | status: 'NEED_UPLOAD' | 'UPLOADING' | 'UPLOADED'; 15 | }[]; 16 | passcode?: string; 17 | exp?: number; 18 | label?: string; 19 | v?: number; 20 | } 21 | 22 | export class SHLClient { 23 | async toLink(shl: SHLAdminParams): Promise { 24 | const shlinkJsonPayload = { 25 | url: `${API_BASE}/shl/${shl.id}`, 26 | exp: shl.exp || undefined, 27 | flag: shl.passcode ? 'P' : '', 28 | key: shl.encryptionKey, 29 | label: shl.label 30 | }; 31 | 32 | const encodedPayload: string = base64url.encode(JSON.stringify(shlinkJsonPayload)); 33 | const shlinkBare = VIEWER_BASE + `shlink:/` + encodedPayload; 34 | return shlinkBare; 35 | } 36 | 37 | async createShl(config: ConfigForServer = {}): Promise { 38 | const ek = randomStringWithEntropy(); 39 | const create = await fetch(`${API_BASE}/shl`, { 40 | method: 'POST', 41 | headers: { 42 | 'content-type': 'application/json' 43 | }, 44 | body: JSON.stringify(config) 45 | }); 46 | const { id, managementToken } = await create.json(); 47 | return { 48 | id, 49 | managementToken, 50 | encryptionKey: ek, 51 | files: [], 52 | ...config 53 | }; 54 | } 55 | 56 | async addFile(shl: SHLAdminParams, content: unknown, contentType: string): Promise { 57 | let contentEncrypted = await new jose.CompactEncrypt( 58 | new TextEncoder().encode(JSON.stringify(content)) 59 | ) 60 | .setProtectedHeader({ 61 | alg: 'dir', 62 | enc: 'A256GCM' 63 | }) 64 | .encrypt(jose.base64url.decode(shl.encryptionKey)); 65 | 66 | new TextEncoder().encode(contentEncrypted), 67 | shl.files.push({ contentEncrypted, contentType, status: 'NEED_UPLOAD' }); 68 | const add = await fetch(`${API_BASE}/shl/${shl.id}/file`, { 69 | method: 'POST', 70 | headers: { 71 | 'content-type': contentType, 72 | authorization: `Bearer ${shl.managementToken}` 73 | }, 74 | body: contentEncrypted 75 | }); 76 | return shl; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/AddFile.svelte: -------------------------------------------------------------------------------- 1 | 74 | 75 |
fetchIps(summaryUrlValidated)}> 76 | 77 | 84 |
85 | 86 | 91 | -------------------------------------------------------------------------------- /static/ips/templates/Medications.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | Current Medications 7 |
8 |
9 |
    10 | {{each(options.medications)}} 11 |
  • 12 | {{if(options.medications[@index].medication.code) }} 13 | 14 | {{if (options.medications[@index].medication.code.coding && options.medications[@index].medication.code.coding.length)}} 15 | {{each(options.medications[@index].medication.code.coding)}} 16 | {{@this.system}} {{@this.display}} 17 | {{if (@this.code)}} 18 | ({{@this.code}}) 19 | {{ /if }} 20 |
    21 | {{/each}} 22 | {{#else}} 23 | Uncoded {{@this.medication.code.text}})
    24 | {{/if}} 25 | {{/if}} 26 | 27 |
    28 | {{if(options.medications[@index].medication.ingredient && options.medications[@index].medication.ingredient.itemCodeableConcept)}} 29 | {{each(options.medications[@index].medication.ingredient)}} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
    Composition
    IngredientStrength Numerator QtyUnitStrength Denominator QtyStrength Denominator Unit
    {{@this.itemCodeableConcept.coding[0].display}}{{@this.strength.numerator.value}}{{@this.strength.numerator.unit}}{{@this.strength.denominator.value}}{{@this.strength.denominator.unit}}
    49 | {{/each}} 50 | {{/if}} 51 | {{if(options.medications[@index].statement.dosage && options.medications[@index].statement.dosage[0].route && options.medications[@index].statement.dosage[0].route.coding && options.medications[@index].statement.dosage[0].doseAndRate)}} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {{if(options.medications[@index].statement.dosage[0].timing && options.medications[@index].statement.dosage[0].timing.repeat)}} 68 | 69 | 70 | {{/if}} 71 | 72 |
    Posología
    RouteQtyUnitFreq. QtyFreq. Period
    {{@this.statement.dosage[0].route.coding[0].display}}{{@this.statement.dosage[0].doseAndRate[0].doseQuantity.value}}{{@this.statement.dosage[0].doseAndRate[0].doseQuantity.unit}}{{@this.statement.dosage[0].timing.repeat.count}}{{@this.statement.dosage[0].timing.repeat.periodUnit}}
    73 |
  • 74 | {{/if}} 75 | {{/each}} 76 |
77 |
78 |
-------------------------------------------------------------------------------- /static/ips/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 22 | 23 | 24 | 25 | IPS Viewer 26 | 27 | 28 | 247 | 278 | 279 |

International Patient Summary (IPS) Viewer for Connectathon

280 |
281 | Links to published Implementation Guide and the latest CI build 282 |
283 |
284 | Please note that this tool is an open-source project under development. It only renders the following sections of IPS bundles: Allergies, Immunizations, Medications, Problems and Results 285 |
286 |
287 |

Submit Data

This is for test data only. Please do not submit PHI.
288 |
289 |
290 |
291 |
292 |
293 | 294 |
295 | 296 |
297 | 298 | 299 | 300 | 301 | 304 | 312 | 313 | 314 |          315 |
316 |
317 |
318 | 319 |
320 | Repository of IPS Samples 321 |
322 |
323 |
324 |
325 |

View IPS

326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 | 357 | 358 | 367 | 368 | --------------------------------------------------------------------------------