UNPKG

@i4mi/js-on-fhir

Version:

A JavaScript wrapper for the I4MI FHIR library.

732 lines 34.8 kB
import { apiCall, HttpMethod, ApiMethods } from '@i4mi/fhir_r4'; import forge from 'node-forge'; export var FHIR_VERSION; (function (FHIR_VERSION) { FHIR_VERSION["STU3"] = "3.0.2"; FHIR_VERSION["R4"] = "4.0.1"; FHIR_VERSION["R4B"] = "4.3.0"; FHIR_VERSION["R5"] = "5.0.0"; })(FHIR_VERSION || (FHIR_VERSION = {})); const DEFAULT_SETTINGS = { urls: { service: '', conformance: '', auth: '', redirect: '', token: '' }, settings: { client: '', userId: '', scope: '', state: '', language: '', supportedResourceTypes: new Array(), fhirVersion: FHIR_VERSION.R4, responseType: 'code', noAuth: false, noPkce: false, codeVerifier: '', codeChallenge: '' }, auth: { accessToken: '', expires: 0, type: '', refreshToken: '' } }; export class JSOnFhir { /** * Creates a new JSOnFhir object and persists it in the sessionStorage so that is * still available after a page reload. * @param serverUrl The URL of the fhir server, e.g. 'https://test.midata.coop'. * @param clientId The ID of your fhir application as registered with the fhir server. * @param redirectUrl The URL where the callback of the OAuth 2.0 can be directed. * @param options? Optional parameter. Options object literal. * @param options.doesNotNeedAuth? Optional parameter. Set to true when the FHIR server you're * using doesn't require authentication (e.g. when connecting to * the EPD playground via MobileAccessGateway). In this case, the * parameters clientId and redirectUrl do not matter (but still * have to be set.) * @param options.disablePkce? Optional parameter. Set to true if you want to use the OAuth 2.0 * authorization code flow instead of the recommended and more secure PKCE flow * or the server does not support PKCE. * @param options.fhirVersion? Set FHIR version to use. Support of versions can be limited on the server used. * Defaults to R4 / 4.0.1. * Possibilities: STU3 (3.0.2), R4 (4.0.1), R4B (4.3.0), R5 (5.0.0) */ constructor(serverUrl, clientId, redirectUrl, options) { this.iife = (() => { var jsonfhir; return { initialize: (store) => { jsonfhir = store; }, jsOnFhir: () => { return jsonfhir; } }; })(); this.apiMethods = new ApiMethods(); const storageKey = this.createStorageKey(serverUrl, clientId); if (!options) { options = { doesNotNeedAuth: false, disablePkce: false, fhirVersion: FHIR_VERSION.R4 }; } else if (options) { if (typeof options.doesNotNeedAuth === 'undefined') { options.doesNotNeedAuth = false; } if (typeof options.disablePkce === 'undefined') { options.disablePkce = false; } } // Check if there is a jsOnFhir object in sessionStorage. let persisted = this.getFromStorage(storageKey); if (!persisted || !(persisted.urls.redirect === redirectUrl && persisted.urls.service === serverUrl + '/fhir' && persisted.settings.client === clientId)) { persisted = DEFAULT_SETTINGS; // If no JSOnFhir object could be loaded from sessionStorage, or if it has different parameters // we assign the URLs and settings properties and save it to the sessionStorage. persisted.urls.service = serverUrl + '/fhir'; persisted.urls.conformance = serverUrl + '/fhir/metadata'; persisted.urls.redirect = redirectUrl; persisted.settings.client = clientId; persisted.settings.scope = 'user/*.*'; persisted.settings.noAuth = options.doesNotNeedAuth ? options.doesNotNeedAuth : false; persisted.settings.noPkce = options.disablePkce ? options.disablePkce : false; } if (options.fhirVersion) { persisted.settings.fhirVersion = options.fhirVersion; this.apiMethods.differentiateContentType('application/fhir+json;fhirVersion=' + options.fhirVersion); } else { persisted.settings.fhirVersion = FHIR_VERSION.R4; } this.iife.initialize(persisted); this.storageKey = storageKey; this.persist(storageKey); } /** * This function starts the OAuth 2.0 authentication procedure, by opening the auth * page for the client to enter his login credentials. Default OAuth 2.0 PKCE grant type is used. * @param params? Optional parameter. An object with key/value pairs. * Can be used to control the login process or may be used * to pre-fill the login or registration form. * @returns nothing * @throws An Error if the conformance statement could not be fetched from the server. */ authenticate(params) { if (this.iife.jsOnFhir().settings.noAuth) { return; } // If PKCE isn't disabled, creates a code verifier and code challenge // based on the former according to rfc7636 section 4.1. if (!this.iife.jsOnFhir().settings.noPkce) { this.iife.jsOnFhir().settings.codeVerifier = this.generateCodeVerifier(); this.iife.jsOnFhir().settings.codeChallenge = this.generateCodeChallenge(this.iife.jsOnFhir().settings.codeVerifier); } // Creates an opaque value used by the client to maintain state between the request and callback. this.iife.jsOnFhir().settings.state = this.generateState(); // Fetching of the auth and token URL. this.fetchConformanceStatement() .then(() => { // Build the authorization request according to rfc6749 section 4.1.1. let authUrl = this.iife.jsOnFhir().urls.auth + '?response_type=' + this.iife.jsOnFhir().settings.responseType + '&client_id=' + encodeURIComponent(this.iife.jsOnFhir().settings.client) + '&scope=' + encodeURIComponent(this.iife.jsOnFhir().settings.scope) + '&redirect_uri=' + encodeURIComponent(this.iife.jsOnFhir().urls.redirect) + '&state=' + this.iife.jsOnFhir().settings.state; // If PKCE isn't disabled, appends the code challenge to the authorization request according to rfc7636 section 4.3. if (!this.iife.jsOnFhir().settings.noPkce) { authUrl += '&code_challenge=' + this.iife.jsOnFhir().settings.codeChallenge + '&code_challenge_method=S256'; } // If language exists and is set in settings property, sets the language to be used on the login page. if (this.iife.jsOnFhir().settings.language.length === 2) { authUrl += '&language=' + encodeURIComponent(this.iife.jsOnFhir().settings.language); } // Append parameter(s) if they exist. if (params) { Object.keys(params).forEach((key) => { authUrl += '&' + key + '=' + encodeURIComponent(params[key].toString()); }); } // Go to authorization (login) page to make an authorization request. window.location.href = authUrl; }) .catch((error) => { throw new Error(error.status + ' ' + error.message); }); } /** * This function handles the authorization response from the auth server according * to rfc6749 section 4.1.2 after the authenticate() function was called. This function must be * called from the same page that the redirectUrl (as defined in the constructor) refers to. * This function then makes an authorization request to the token endpoint as described in * rfc6749 section 4.1.3. * @returns A promise when called after authenticate(): * - fulfilled: Response of token endpoint. * - rejected: An error message. */ handleAuthResponse() { return new Promise((resolve, reject) => { // When the URL has #, window.location.search is empty, and we have to parse windows.location.hash. let paramString = window.location.search.length > 0 ? window.location.search : '?' + window.location.hash.split('?')[1]; // Check whether paramString includes state and authorization code parameters. if (paramString.includes('state=') && paramString.includes('code=')) { // Create array containing [state=abc,code=xyz] const urlParams = paramString.substring(1).split('&'); // Get state and authorization code from urlParams. let state, authCode; urlParams.forEach((entry) => { const param = entry.split('='); if (param[0] === 'state') { state = decodeURIComponent(param[1].replace(/\+/g, '%20')); } if (param[0] === 'code') { authCode = decodeURIComponent(param[1].replace(/\+/g, '%20')); } }); // Request an access and refresh token from the token endpoint. this.tokenRequest(state, authCode) .then((response) => { // Resets the URL so that we don't run into misinterpreting the same code later again. history.pushState({}, null, this.iife.jsOnFhir().urls.redirect); resolve(response); }) .catch((error) => { reject(new Error(error.status + ' ' + error.message)); }); } else { resolve(null); } }); } /** * Given the prerequisite that the local state and server response state are the same * and therefore no cross-site request forgery attack is being performed, this function * makes a request to the token endpoint as described in rfc6749 section 4.1.3, or if PKCE isn't disabled * as described in rfc7636 section 4.5. * The token endpoints response is then handled by the handleTokenResponse() function. * @param state An opaque value used by the client to maintain state between the request and callback. * @param authCode The authorization code received from the authorization server. * @returns A promise: * - fulfilled: Response of the token endpoint. * - rejected: Error message. */ tokenRequest(state, authCode) { return new Promise((resolve, reject) => { // Check if local state and the server's response state are the same. if (this.iife.jsOnFhir().settings.state === state) { let data = 'grant_type=authorization_code' + '&redirect_uri=' + encodeURIComponent(this.iife.jsOnFhir().urls.redirect) + '&client_id=' + encodeURIComponent(this.iife.jsOnFhir().settings.client) + '&code=' + authCode; // If PKCE isn't disabled, append the code verifier to the data string. if (!this.iife.jsOnFhir().settings.noPkce) { data += '&code_verifier=' + this.iife.jsOnFhir().settings.codeVerifier; } // Make request to the token endpoint using apiCall function from @i4mi/fhir_r4. apiCall({ url: this.iife.jsOnFhir().urls.token, method: HttpMethod.POST, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, jsonBody: true, payload: data, jsonEncoded: false }).then((response) => { if (response.status === 200) { // Handle the response of the token endpoint. this.handleTokenResponse(response); resolve(response.body); } else { reject(new Error(response.status + ' ' + response.message)); } }); } else { reject(new Error('Server response state and local state mismatch.')); } }); } /** * Refreshes an access token, given that the client is in possession of a refresh token * issued by the authorization server. The token endpoint's response is then handled by * the handleTokenResponse() function. * @returns A promise: * - fulfilled: Response from token endpoint or empty promise if server doesn't require authentication. * - rejected: Error message. */ refreshAuth(refreshToken) { return new Promise((resolve, reject) => { // Resolve promise if doesNotNeedAuth? was set to true in the constructor. if (this.iife.jsOnFhir().settings.noAuth) { Promise.resolve(); } // Check that the provided refreshToken is valid. if (refreshToken && refreshToken !== '') { // Make request to the token endpoint. apiCall({ url: this.iife.jsOnFhir().urls.token, method: HttpMethod.POST, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, jsonBody: true, payload: 'grant_type=refresh_token&refresh_token=' + this.iife.jsOnFhir().auth.refreshToken, jsonEncoded: false }).then((response) => { if (response.status === 200) { // Handle the response of the token endpoint. this.handleTokenResponse(response); resolve(response.body); } else { reject(new Error(response.status + ' ' + response.message)); } }); } else { reject(new Error('Invalid refresh token.')); } }); } /** * Checks whether an access token is set and has not yet expired. * @returns boolean: * - true: Access token is valid. * - false: Access token is invalid. Will also return * false when doesNotNeedAuth? was set true in the constructor. */ isLoggedIn() { return this.iife.jsOnFhir().auth.accessToken !== '' && this.iife.jsOnFhir().auth.expires > Date.now(); } /** * Logs out the user by deleting certain settings and permission properties. */ logout() { this.iife.jsOnFhir().settings = Object.assign(Object.assign({}, this.iife.jsOnFhir().settings), { userId: '', codeVerifier: '', codeChallenge: '', state: '' }); this.iife.jsOnFhir().auth = { accessToken: '', expires: 0, type: '', refreshToken: '' }; this.persist(this.storageKey); } /** * Helper function to get a ApiConfig object consisting of the access token, authorization type and the base URL. * @returns ApiConfig object. */ getApiConfig() { return { access_token: this.iife.jsOnFhir().auth.accessToken, authorization_type: this.iife.jsOnFhir().auth.type, base_url: this.iife.jsOnFhir().urls.service }; } /** * Creates a resource on the fhir server. * @param resource Resource to be saved. * @returns A promise: * - fulfilled: Representation of fhir resource in JSON format. * - rejected: Error message. */ create(resource) { return new Promise((resolve, reject) => { // Reject promise if user is not logged in and doesNotNeedAuth? was set to false in the constructor. if (!(this.isLoggedIn() || this.iife.jsOnFhir().settings.noAuth)) { reject(new Error('User not logged in.')); } // Create a resource on the fhir server by calling the create() function of apiMethods. this.apiMethods .create(resource, this.getApiConfig()) .then((response) => { response.status === 200 || response.status === 201 ? resolve(JSON.parse(response.body)) : reject(response); }) .catch((error) => { this.handleError(error); reject(new Error(error.status + ' ' + error.message)); }); }); } /** * Updates a resource on the fhir server. * @param resource Resource to be updated. * @returns A promise: * - fulfilled: Representation of fhir resource in JSON format. * - rejected: Error message. */ update(resource) { return new Promise((resolve, reject) => { // Reject promise if user is not logged in and doesNotNeedAuth? was set to false in the constructor. if (!(this.isLoggedIn() || this.iife.jsOnFhir().settings.noAuth)) { reject(new Error('User not logged in.')); } // Reject promise if resource ID is undefined. if (typeof resource.id === 'undefined' && typeof resource._id === 'undefined') { reject(new Error('Resource has no id.')); } // Update resource on the fhir server by calling the update() function of apiMethods. this.apiMethods .update(resource, this.getApiConfig()) .then((response) => { response.status === 200 || response.status === 201 ? resolve(JSON.parse(response.body)) : reject(response); }) .catch((error) => { this.handleError(error); reject(new Error(error.status + ' ' + error.message)); }); }); } /** * Searches for resources matching the given criteria in params. * @param resourceType ResourceType to be searched. * @param params? Optional parameter. Search parameters according to fhir resource guide. * @returns A promise: * - fulfilled: Bundle of type searchset containing the fhir resource(s). * - rejected: Error message. */ search(resourceType, params) { if (this.iife.jsOnFhir().settings.supportedResourceTypes.findIndex((r) => r === resourceType) === -1) { return Promise.reject('ResourceType "' + resourceType + '" is not supported by this server.' + (resourceType.includes('/') ? ' For directly fetching a resource use getResource().' : '')); } return new Promise((resolve, reject) => { // Reject promise if user is not logged in and doesNotNeedAuth? was set to false in the constructor. if (!(this.isLoggedIn() || this.iife.jsOnFhir().settings.noAuth)) { reject(new Error('User not logged in.')); } // Search for one specific or multiple resources on the fhir server by calling the search() function of apiMethods. this.apiMethods .search(params, resourceType, this.getApiConfig()) .then((response) => { response.status === 200 || response.status === 201 ? resolve(JSON.parse(response.body)) : reject(response); }) .catch((error) => { this.handleError(error); reject(new Error(error.status + ' ' + error.message)); }); }); } /** * Fetches a given known resource. * @param resourceType ResourceType of the resource. * @param id The unique id of the resource. * @returns A promise: * - fulfilled: Bundle of type searchset containing the fhir resource(s). * - rejected: Error message. */ getResource(resourceType, id) { return new Promise((resolve, reject) => { // Reject promise if user is not logged in and doesNotNeedAuth? was set to false in the constructor. if (!(this.isLoggedIn() || this.iife.jsOnFhir().settings.noAuth)) { reject(new Error('User not logged in.')); } // Search for one specific or multiple resources on the fhir server by calling the search() function of apiMethods. this.apiMethods .search(null, resourceType + '/' + id, this.getApiConfig()) .then((response) => { response.status === 200 || response.status === 201 ? resolve(JSON.parse(response.body)) : reject(response); }) .catch((error) => { this.handleError(error); reject(new Error(error.status + ' ' + error.message)); }); }); } /** * Performs a given operation on the fhir server. * @param operation The type of the operation (e.g 'process-message'). * @param payload? Optional Parameter. A resource or other payload to process in the operation. * @param httpMethod Optional Parameter. The HTTP method to be used (GET|POST|PUT|DELETE). Default is HTTP method (POST). * @param params? Optional Parameter. Parameters, either as key/value pair or as a string. * @param resourceType? Optional Parameter. Specify the resource type on which the operation is to be performed (mandatory if resourceId is used). * @param resourceId? Optional Parameter. Specify an instance of a resource for which the operation is to be performed. * @returns A promise: * - fulfilled: Response of the fhir server. * - rejected: Error message. */ performOperation(operation, payload, httpMethod = HttpMethod.POST, params, resourceType, resourceId) { return new Promise((resolve, reject) => { // Reject promise if user is not logged in and doesNotNeedAuth? was set to false in the constructor. if (!(this.isLoggedIn() || this.iife.jsOnFhir().settings.noAuth)) { reject(new Error('User not logged in.')); } // Reject promise if resourceInstance (resourceId) but no resourceType is provided. if (resourceId && !resourceType) { reject(new Error('Instance of resource is provided, but not the resourceType.')); } // Create paramUrl. let paramUrl = ''; // Append parameters from key/value pairs or from a string. if (params) { paramUrl += '?'; if (typeof params === 'string') { paramUrl += encodeURI(params); } else { Object.keys(params).forEach((key, index) => { paramUrl += index === 0 ? key + '=' + encodeURI(params[key]) : '&' + (key + '=' + encodeURI(params[key])); }); } } // Set '/' prefix to resourceType and resourceId if they exist. If they don't exist, set them both to ''. resourceType = resourceType ? '/' + resourceType : ''; resourceId = resourceId ? '/' + resourceId : ''; // Perform operation on fhir server by using apiCall function from @i4mi/fhir_r4. apiCall({ url: this.iife.jsOnFhir().urls.service + resourceType + resourceId + '/$' + operation + paramUrl, method: httpMethod, headers: { 'Content-Type': 'application/fhir+json;' + (this.iife.jsOnFhir().settings.fhirVersion && this.iife.jsOnFhir().settings.fhirVersion.length > 0 ? ' fhirVersion=' + this.iife.jsOnFhir().settings.fhirVersion : ''), Authorization: 'Bearer ' + this.getAccessToken() }, jsonBody: true, payload: payload, jsonEncoded: typeof payload !== 'string' }) .then((response) => { resolve(typeof response === 'string' ? JSON.parse(response).body : response.body); }) .catch((error) => { reject(new Error(error.status + ' ' + error.message)); }); }); } /** * Sets the language for the authorization request window. * @param lang The language as two-char string (eg. 'de', 'en', 'fr' or 'it'). */ setLanguage(lang) { if (lang.length === 2) { this.iife.jsOnFhir().settings.language = lang.toLowerCase(); this.persist(this.storageKey); } else { throw new Error('The supplied language code is not a two-char string.'); } } /** * Sets the conformance URL, if it differs from the default (serverURL + '/fhir/metadata'). * @param conformanceUrl The new conformance statement URL. */ setConformanceUrl(conformanceUrl) { this.iife.jsOnFhir().urls.conformance = conformanceUrl; this.persist(this.storageKey); } /** * Sets the scope when it differs from the default 'user/*.*'. * @param scope The scope as string. */ setScope(scope) { this.iife.jsOnFhir().settings.scope = scope; this.persist(this.storageKey); } /** * Returns the resource id of the Patient or Practitioner resource of the logged in user * @return the Patient Resource ID as a string, if logged in * @return undefined if not logged in * @deprecated use getUserId() instead */ getPatient() { console.warn('js-on-fhir: getPatient() is deprecated since V1.0.0 and will be removed in a later versions.\nUse getUserId() instead.'); return this.getUserId(); } /** * Returns the resource id of the of the logged in user. With this id, you can then fetch the * Patient (for normal user) or Practitioner (for health professionals or researcher). * @return the Patient Resource ID as a string, if logged in * undefined if not logged in */ getUserId() { if (this.iife.jsOnFhir().settings.userId && this.iife.jsOnFhir().settings.userId !== '') { return this.iife.jsOnFhir().settings.userId; } else { return undefined; } } /** * Creates an opaque value used by the client to maintain state * between the request and callback. Is used for preventing * cross-site request forgery according to rfc 6749 section 4.1.1. * @returns state (url-safe) with a length of 128 characters. */ generateState() { return encodeURIComponent(forge.util.encode64(forge.random.getBytesSync(128))) .replace(/%/g, '') .substring(0, 128); } /** * Generates a code verifier according to rfc7636 section 4.1. * The code verifier is a high-entropy cryptographic URL safe random string * using the unreserved characters from rfc3986 section 2.3, * with a minimum length of 43 characters and a maximum length of 128 characters. * @returns code verifier (url-safe) with a length of 128 characters. */ generateCodeVerifier() { return encodeURIComponent(forge.util.encode64(forge.random.getBytesSync(128))) .replace(/%/g, '') .substring(0, 128); } /** * Creates a code challenge derived from the code verifier according to rfc 7636 section 4.2. * The code transformation used on the code verifier is SHA256. The result is then Base64 encoded. * @param codeVerifier high-entropy cryptographic URL safe random string using the unreserved characters from rfc3986 section 2.3. * @returns code challenge (hashed and Base64 encoded code verifier). */ generateCodeChallenge(codeVerifier) { return forge.util .encode64(forge.util.hexToBytes(forge.md.sha256.create().update(forge.util.decodeUtf8(codeVerifier)).digest().toHex())) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); } /** * Fetches the auth and token URL as well as the supported fhir version from the conformance statement endpoint (default=/fhir/metadata). * Also fetches the fhir version supported by the server. * @returns A promise: * - fulfilled: Response of the conformance statement request. * - rejected: Error message. */ fetchConformanceStatement() { return new Promise((resolve, reject) => { // Set the conformance endpoint URL. const cfUrl = typeof this.iife.jsOnFhir().urls.conformance !== 'undefined' ? this.iife.jsOnFhir().urls.conformance : this.iife.jsOnFhir().urls.service + '/metadata'; // Make request to the conformance endpoint using apiCall function from @i4mi/fhir_r4. apiCall({ url: cfUrl, headers: { 'Content-Type': 'application/fhir+json;' + (this.iife.jsOnFhir().settings.fhirVersion && this.iife.jsOnFhir().settings.fhirVersion.length > 0 ? ' fhirVersion=' + this.iife.jsOnFhir().settings.fhirVersion : '') }, method: HttpMethod.GET }) .then((response) => { if (response.status === 200) { // Handle the response of the conformance statement request. this.handleConformanceStatementResponse(response); resolve(response); } else { reject(new Error(response.status + ' ' + response.message)); } }) .catch((error) => { reject(new Error(error.status + ' ' + error.message)); }); }); } /** * Getter Function that returns the current access token, if available. * @returns access Token */ getAccessToken() { return this.iife.jsOnFhir().auth && this.iife.jsOnFhir().auth.accessToken ? this.iife.jsOnFhir().auth.accessToken : undefined; } /** * Changes the FHIR version used to do the requests to the server. * Note that the available versions may be restricted on your server. * @param version The FHIR version to use. Support of versions can be restricted on the server used. * Supported versions: STU3 (3.0.2), R4 (4.0.1), R4B (4.3.0), R5 (5.0.0) */ changeFhirVersion(version) { this.iife.jsOnFhir().settings.fhirVersion = version; this.apiMethods.differentiateContentType('application/fhir+json;fhirVersion=' + version); } /** * Handles the response of the conformance statement request by saving the relevant data. * @param response Response of the conformance Request. */ handleConformanceStatementResponse(response) { response.body = JSON.parse(response.body); this.iife.jsOnFhir().urls.token = response.body.rest['0'].security.extension['0'].extension['0'].valueUri; this.iife.jsOnFhir().urls.auth = response.body.rest['0'].security.extension['0'].extension['1'].valueUri; this.iife.jsOnFhir().settings.supportedResourceTypes = response.body.rest['0'].resource.map((r) => r.type); this.iife.jsOnFhir().settings.fhirVersion = response.body.fhirVersion; this.persist(this.storageKey); } /** * Handles the token endpoint response by saving the relevant data from the access token request. * @param response Response of the access token request. */ handleTokenResponse(response) { this.iife.jsOnFhir().auth.accessToken = response.body.access_token; this.iife.jsOnFhir().auth.expires = Date.now() + 1000 * response.body.expires_in; this.iife.jsOnFhir().auth.type = response.body.token_type; this.iife.jsOnFhir().auth.refreshToken = response.body.refresh_token; this.iife.jsOnFhir().settings.userId = response.body.patient; this.persist(this.storageKey); } /** * Helper function for creating a storage key that is unique for a server / client combination. */ createStorageKey(serverUrl, clientId) { return forge.util.encode64(forge.util.decodeUtf8(encodeURIComponent(serverUrl + clientId))); } /** * Helper function that saves the JSOnFhir object to sessionStorage. It is used to restore * the JSOnFhir object after a page reload (e.g. after the authenticate() function was called). */ persist(key) { if (!key) throw new Error('Can not persist without key'); const toStore = this.iife.jsOnFhir(); sessionStorage.setItem(key, JSON.stringify(toStore)); } getFromStorage(key) { const fromStorage = sessionStorage.getItem(key); return fromStorage ? JSON.parse(fromStorage) : null; } /** * Helper function to handle errors (e.g. expired access token). * @param error Error object. */ handleError(error) { if (error.body === 'Invalid token' || error.body === 'Expired token' || error.status === 401) { this.logout(); } } } //# sourceMappingURL=js-on-fhir.js.map