Skip to content

Commit

Permalink
Move generic SHC functions and IPS resource extraction function to ut…
Browse files Browse the repository at this point in the history
…il file
  • Loading branch information
daniellrgn committed Oct 17, 2024
1 parent e5ed740 commit 77a6fc8
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 96 deletions.
83 changes: 6 additions & 77 deletions src/lib/AddFile.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script lang="ts">
import * as jose from 'jose';
import * as pako from 'pako';
import { createEventDispatcher, onMount } from 'svelte';
import { page } from '$app/stores';
import { getResourcesFromIPS } from './resourceUploader.js';
import {
getResourcesFromIPS,
isSHCFile,
packageSHC,
} from '$lib/util';
import { goto } from '$app/navigation';
import { getContext } from 'svelte';
import { type Writable } from 'svelte/store';
Expand All @@ -30,14 +32,13 @@
import FetchTEFCA from './FetchTEFCA.svelte';
import ODHForm from './ODHForm.svelte';
import { verify } from './shcDecoder.js';
import issuerKeys from './issuer.private.jwks.json';
import type { SHCFile,
SHCRetrieveEvent,
ResourceRetrieveEvent,
IPSRetrieveEvent,
SHLSubmitEvent,
SOFAuthEvent } from './types';
import type { Patient, Bundle } from 'fhir/r4';
import type { Patient } from 'fhir/r4';
import { IPSResourceCollection } from './IPSResourceCollection.js';
import ResourceSelector from './ResourceSelector.svelte';
Expand Down Expand Up @@ -241,20 +242,6 @@
fetchError = "Error parsing IPS";
}
}
function isSHCFile(object: any): object is SHCFile {
return 'verifiableCredential' in object;
}
async function packageSHC(content:SHCFile | Bundle | undefined): Promise<SHCFile> {
if (content != undefined && isSHCFile(content) && content.verifiableCredential) {
return content;
}
const shc = await signJws(content);
return { verifiableCredential: [shc] };
}
async function submitSHL() {
return shlDispatch('shl-submitted', {
Expand All @@ -266,29 +253,6 @@
});
}
const exampleSigningKey = jose.importJWK(issuerKeys.keys[0]);
async function signJws(payload: unknown) {
const fields = { zip: 'DEF', alg: 'ES256', kid: issuerKeys.keys[0].kid };
const body = pako.deflateRaw(
JSON.stringify({
iss: 'https://spec.smarthealth.cards/examples/issuer',
nbf: new Date().getTime() / 1000,
vc: {
type: ['https://smarthealth.cards#health-card'],
credentialSubject: {
fhirVersion: '4.0.1',
fhirBundle: payload
}
}
})
);
const signed = new jose.CompactSign(body)
.setProtectedHeader(fields)
.sign(await exampleSigningKey);
return signed;
}
async function showSuccessMessage() {
successMessage = true;
setTimeout(() => {
Expand Down Expand Up @@ -493,41 +457,6 @@
</Row>
{/if}
<span class="text-danger">{fetchError}</span>
{#if resourcesAdded}
{#if false && ipsResult.ips}
<Row class="align-items-center">
<Col xs="auto">
<Button
color="secondary"
style="width:fit-content"
disabled={submitting}
type="button"
on:click={() => {uploadRetrievedIPS(ipsResult)}}>
{#if !submitting}
Submit Unchanged IPS
{:else}
Submitting...
{/if}
</Button>
</Col>
{#if submitting}
<Col xs="auto" class="d-flex align-items-center px-0">
<Spinner color="primary" type="border" size="md"/>
</Col>
{/if}
<Col xs="auto">
<Card color="light">
<CardBody>
<CardText color="light" style="overflow: hidden; text-overflow: ellipsis">
<Icon name="file-earmark-text" /> {ipsResult.source}
</CardText>
</CardBody>
</Card>
</Col>
</Row>
<br/>
{/if}
{/if}
{/if}
{#if $mode === "advanced"}
<br>
Expand Down
18 changes: 0 additions & 18 deletions src/lib/resourceUploader.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
import { INTERMEDIATE_FHIR_SERVER_BASE } from './config';

export function getResourcesFromIPS(ips) {
let entries = ips.entry;
let resources = [];
entries.forEach((entry) => {
// if (entry.resource.resourceType == 'Condition') return; // Omit conditions until ips fhir server is upgraded
if (entry.resource.resourceType == 'Composition') return;

entry.resource.id = entry.fullUrl;
if (entry.resource.extension) {
entry.resource.extension = entry.resource.extension.filter(function(item) {
return item.url !== "http://hl7.org/fhir/StructureDefinition/narrativeLink";
})
}
resources.push(entry.resource);
});
return resources;
}

// Create Bundle and POST
export async function uploadResources(resources) {
let entries = [];
Expand Down
63 changes: 62 additions & 1 deletion src/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import * as jose from 'jose';
import * as pako from 'pako';
import issuerKeys from './issuer.private.jwks.json';
import type { SHCFile } from './types';
import type { Bundle, BundleEntry, Resource } from 'fhir/r4';

export const base64url = jose.base64url;

Expand All @@ -11,4 +15,61 @@ export function randomStringWithEntropy(entropy = 32): string {
export async function base64toBlob(base64:string, type="application/octet-stream") {
let result = await fetch(`data:${type};base64,${base64}`);
return window.URL.createObjectURL(await result.blob());
}
}

export function getResourcesFromIPS(ips: Bundle) {
let entries = ips.entry;
let resources = [] as Resource[];
if (!entries) return resources;
entries.forEach((entry: BundleEntry) => {
if (!entry.resource) return;
// if (entry.resource.resourceType == 'Condition') return; // Omit conditions until ips fhir server is upgraded
if (entry.resource.resourceType == 'Composition') return;

entry.resource.id = entry.fullUrl;
if ('extension' in entry.resource && entry.resource.extension) {
entry.resource.extension = entry.resource.extension.filter(function(item) {
return item.url !== "http://hl7.org/fhir/StructureDefinition/narrativeLink";
})
}
resources.push(entry.resource);
});
return resources;
}

export function isSHCFile(object: any): object is SHCFile {
return 'verifiableCredential' in object;
}

const exampleSigningKey = jose.importJWK(issuerKeys.keys[0]);
export async function signJws(payload: unknown) {
const fields = { zip: 'DEF', alg: 'ES256', kid: issuerKeys.keys[0].kid };
const body = pako.deflateRaw(
JSON.stringify({
iss: 'https://spec.smarthealth.cards/examples/issuer',
nbf: new Date().getTime() / 1000,
vc: {
type: ['https://smarthealth.cards#health-card'],
credentialSubject: {
fhirVersion: '4.0.1',
fhirBundle: payload
}
}
})
);

const signed = new jose.CompactSign(body)
.setProtectedHeader(fields)
.sign(await exampleSigningKey);
return signed;
}

export async function packageSHC(content:SHCFile | Bundle | undefined): Promise<SHCFile> {
if (content != undefined && isSHCFile(content) && content.verifiableCredential) {
return content;
}

const shc = await signJws(content);

return { verifiableCredential: [shc] };
}

0 comments on commit 77a6fc8

Please sign in to comment.