@medplum/core 2.0.14 → 2.0.15
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/cjs/index.cjs +1155 -1060
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.min.cjs +1 -1
- package/dist/esm/base64.mjs +33 -0
- package/dist/esm/base64.mjs.map +1 -0
- package/dist/esm/cache.mjs +17 -23
- package/dist/esm/cache.mjs.map +1 -1
- package/dist/esm/client.mjs +464 -395
- package/dist/esm/client.mjs.map +1 -1
- package/dist/esm/eventtarget.mjs +6 -11
- package/dist/esm/eventtarget.mjs.map +1 -1
- package/dist/esm/fhirlexer/parse.mjs +15 -23
- package/dist/esm/fhirlexer/parse.mjs.map +1 -1
- package/dist/esm/fhirlexer/tokenize.mjs +190 -180
- package/dist/esm/fhirlexer/tokenize.mjs.map +1 -1
- package/dist/esm/fhirmapper/parse.mjs +264 -252
- package/dist/esm/fhirmapper/parse.mjs.map +1 -1
- package/dist/esm/fhirmapper/tokenize.mjs +0 -1
- package/dist/esm/fhirmapper/tokenize.mjs.map +1 -1
- package/dist/esm/fhirpath/atoms.mjs +0 -1
- package/dist/esm/fhirpath/atoms.mjs.map +1 -1
- package/dist/esm/fhirpath/parse.mjs +0 -1
- package/dist/esm/fhirpath/parse.mjs.map +1 -1
- package/dist/esm/fhirpath/tokenize.mjs +0 -1
- package/dist/esm/fhirpath/tokenize.mjs.map +1 -1
- package/dist/esm/filter/parse.mjs +0 -2
- package/dist/esm/filter/parse.mjs.map +1 -1
- package/dist/esm/filter/tokenize.mjs +0 -1
- package/dist/esm/filter/tokenize.mjs.map +1 -1
- package/dist/esm/index.min.mjs +1 -1
- package/dist/esm/jwt.mjs +2 -9
- package/dist/esm/jwt.mjs.map +1 -1
- package/dist/esm/readablepromise.mjs +18 -23
- package/dist/esm/readablepromise.mjs.map +1 -1
- package/dist/esm/schema.mjs +146 -123
- package/dist/esm/schema.mjs.map +1 -1
- package/dist/esm/search/match.mjs +0 -2
- package/dist/esm/search/match.mjs.map +1 -1
- package/dist/esm/storage.mjs +13 -19
- package/dist/esm/storage.mjs.map +1 -1
- package/dist/types/base64.d.ts +14 -0
- package/dist/types/cache.d.ts +3 -1
- package/dist/types/client.d.ts +127 -1
- package/dist/types/eventtarget.d.ts +1 -1
- package/dist/types/fhirlexer/parse.d.ts +5 -2
- package/dist/types/fhirlexer/tokenize.d.ts +28 -1
- package/dist/types/readablepromise.d.ts +4 -1
- package/dist/types/schema.d.ts +33 -1
- package/dist/types/storage.d.ts +2 -2
- package/package.json +1 -1
- package/dist/esm/node_modules/tslib/tslib.es6.mjs +0 -30
- package/dist/esm/node_modules/tslib/tslib.es6.mjs.map +0 -1
package/dist/esm/client.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { __classPrivateFieldSet, __classPrivateFieldGet } from './node_modules/tslib/tslib.es6.mjs';
|
|
2
1
|
import { LRUCache } from './cache.mjs';
|
|
3
2
|
import { getRandomString, encryptSHA256 } from './crypto.mjs';
|
|
4
3
|
import { EventTarget } from './eventtarget.mjs';
|
|
@@ -8,11 +7,10 @@ import { ReadablePromise } from './readablepromise.mjs';
|
|
|
8
7
|
import { ClientStorage } from './storage.mjs';
|
|
9
8
|
import { globalSchema, indexStructureDefinition, indexSearchParameter } from './types.mjs';
|
|
10
9
|
import { createReference, arrayBufferToBase64 } from './utils.mjs';
|
|
10
|
+
import { encodeBase64 } from './base64.mjs';
|
|
11
11
|
|
|
12
12
|
// PKCE auth based on:
|
|
13
|
-
|
|
14
|
-
var _MedplumClient_instances, _MedplumClient_fetch, _MedplumClient_createPdf, _MedplumClient_storage, _MedplumClient_requestCache, _MedplumClient_cacheTime, _MedplumClient_baseUrl, _MedplumClient_fhirBaseUrl, _MedplumClient_authorizeUrl, _MedplumClient_tokenUrl, _MedplumClient_logoutUrl, _MedplumClient_exchangeUrl, _MedplumClient_onUnauthenticated, _MedplumClient_autoBatchTime, _MedplumClient_autoBatchQueue, _MedplumClient_clientId, _MedplumClient_clientSecret, _MedplumClient_autoBatchTimerId, _MedplumClient_accessToken, _MedplumClient_refreshToken, _MedplumClient_refreshPromise, _MedplumClient_profilePromise, _MedplumClient_profile, _MedplumClient_config, _MedplumClient_addLogin, _MedplumClient_refreshProfile, _MedplumClient_getCacheEntry, _MedplumClient_setCacheEntry, _MedplumClient_cacheResource, _MedplumClient_deleteCacheEntry, _MedplumClient_request, _MedplumClient_fetchWithRetry, _MedplumClient_executeAutoBatch, _MedplumClient_addFetchOptionsDefaults, _MedplumClient_setRequestContentType, _MedplumClient_setRequestBody, _MedplumClient_handleUnauthenticated, _MedplumClient_requestAuthorization, _MedplumClient_refresh, _MedplumClient_fetchTokens, _MedplumClient_verifyTokens, _MedplumClient_setupStorageListener;
|
|
15
|
-
const MEDPLUM_VERSION = "2.0.14-8c7457fd";
|
|
13
|
+
const MEDPLUM_VERSION = "2.0.15-025c3c04";
|
|
16
14
|
const DEFAULT_BASE_URL = 'https://api.medplum.com/';
|
|
17
15
|
const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
|
|
18
16
|
const DEFAULT_CACHE_TIME = 60000; // 60 seconds
|
|
@@ -75,57 +73,44 @@ const system = { resourceType: 'Device', id: 'system', deviceName: [{ name: 'Sys
|
|
|
75
73
|
class MedplumClient extends EventTarget {
|
|
76
74
|
constructor(options) {
|
|
77
75
|
super();
|
|
78
|
-
_MedplumClient_instances.add(this);
|
|
79
|
-
_MedplumClient_fetch.set(this, void 0);
|
|
80
|
-
_MedplumClient_createPdf.set(this, void 0);
|
|
81
|
-
_MedplumClient_storage.set(this, void 0);
|
|
82
|
-
_MedplumClient_requestCache.set(this, void 0);
|
|
83
|
-
_MedplumClient_cacheTime.set(this, void 0);
|
|
84
|
-
_MedplumClient_baseUrl.set(this, void 0);
|
|
85
|
-
_MedplumClient_fhirBaseUrl.set(this, void 0);
|
|
86
|
-
_MedplumClient_authorizeUrl.set(this, void 0);
|
|
87
|
-
_MedplumClient_tokenUrl.set(this, void 0);
|
|
88
|
-
_MedplumClient_logoutUrl.set(this, void 0);
|
|
89
|
-
_MedplumClient_exchangeUrl.set(this, void 0);
|
|
90
|
-
_MedplumClient_onUnauthenticated.set(this, void 0);
|
|
91
|
-
_MedplumClient_autoBatchTime.set(this, void 0);
|
|
92
|
-
_MedplumClient_autoBatchQueue.set(this, void 0);
|
|
93
|
-
_MedplumClient_clientId.set(this, void 0);
|
|
94
|
-
_MedplumClient_clientSecret.set(this, void 0);
|
|
95
|
-
_MedplumClient_autoBatchTimerId.set(this, void 0);
|
|
96
|
-
_MedplumClient_accessToken.set(this, void 0);
|
|
97
|
-
_MedplumClient_refreshToken.set(this, void 0);
|
|
98
|
-
_MedplumClient_refreshPromise.set(this, void 0);
|
|
99
|
-
_MedplumClient_profilePromise.set(this, void 0);
|
|
100
|
-
_MedplumClient_profile.set(this, void 0);
|
|
101
|
-
_MedplumClient_config.set(this, void 0);
|
|
102
76
|
if (options?.baseUrl) {
|
|
103
77
|
if (!options.baseUrl.startsWith('http')) {
|
|
104
78
|
throw new Error('Base URL must start with http or https');
|
|
105
79
|
}
|
|
106
80
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
81
|
+
this.fetch = options?.fetch || getDefaultFetch();
|
|
82
|
+
this.storage = options?.storage || new ClientStorage();
|
|
83
|
+
this.createPdfImpl = options?.createPdf;
|
|
84
|
+
this.baseUrl = ensureTrailingSlash(options?.baseUrl) || DEFAULT_BASE_URL;
|
|
85
|
+
this.fhirBaseUrl = this.baseUrl + (ensureTrailingSlash(options?.fhirUrlPath) || 'fhir/R4/');
|
|
86
|
+
this.clientId = options?.clientId || '';
|
|
87
|
+
this.authorizeUrl = options?.authorizeUrl || this.baseUrl + 'oauth2/authorize';
|
|
88
|
+
this.tokenUrl = options?.tokenUrl || this.baseUrl + 'oauth2/token';
|
|
89
|
+
this.logoutUrl = options?.logoutUrl || this.baseUrl + 'oauth2/logout';
|
|
90
|
+
this.exchangeUrl = this.baseUrl + 'auth/exchange';
|
|
91
|
+
this.onUnauthenticated = options?.onUnauthenticated;
|
|
92
|
+
this.cacheTime = options?.cacheTime ?? DEFAULT_CACHE_TIME;
|
|
93
|
+
if (this.cacheTime > 0) {
|
|
94
|
+
this.requestCache = new LRUCache(options?.resourceCacheSize ?? DEFAULT_RESOURCE_CACHE_SIZE);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.requestCache = undefined;
|
|
98
|
+
}
|
|
99
|
+
if (options?.autoBatchTime) {
|
|
100
|
+
this.autoBatchTime = options?.autoBatchTime ?? 0;
|
|
101
|
+
this.autoBatchQueue = [];
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.autoBatchTime = 0;
|
|
105
|
+
this.autoBatchQueue = undefined;
|
|
106
|
+
}
|
|
122
107
|
const activeLogin = this.getActiveLogin();
|
|
123
108
|
if (activeLogin) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
109
|
+
this.accessToken = activeLogin.accessToken;
|
|
110
|
+
this.refreshToken = activeLogin.refreshToken;
|
|
111
|
+
this.refreshProfile().catch(console.log);
|
|
127
112
|
}
|
|
128
|
-
|
|
113
|
+
this.setupStorageListener();
|
|
129
114
|
}
|
|
130
115
|
/**
|
|
131
116
|
* Returns the current base URL for all API requests.
|
|
@@ -135,14 +120,14 @@ class MedplumClient extends EventTarget {
|
|
|
135
120
|
* @returns The current base URL for all API requests.
|
|
136
121
|
*/
|
|
137
122
|
getBaseUrl() {
|
|
138
|
-
return
|
|
123
|
+
return this.baseUrl;
|
|
139
124
|
}
|
|
140
125
|
/**
|
|
141
126
|
* Clears all auth state including local storage and session storage.
|
|
142
127
|
* @category Authentication
|
|
143
128
|
*/
|
|
144
129
|
clear() {
|
|
145
|
-
|
|
130
|
+
this.storage.clear();
|
|
146
131
|
this.clearActiveLogin();
|
|
147
132
|
}
|
|
148
133
|
/**
|
|
@@ -151,12 +136,12 @@ class MedplumClient extends EventTarget {
|
|
|
151
136
|
* @category Authentication
|
|
152
137
|
*/
|
|
153
138
|
clearActiveLogin() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
139
|
+
this.storage.setString('activeLogin', undefined);
|
|
140
|
+
this.requestCache?.clear();
|
|
141
|
+
this.accessToken = undefined;
|
|
142
|
+
this.refreshToken = undefined;
|
|
143
|
+
this.profile = undefined;
|
|
144
|
+
this.config = undefined;
|
|
160
145
|
this.dispatchEvent({ type: 'change' });
|
|
161
146
|
}
|
|
162
147
|
/**
|
|
@@ -166,7 +151,7 @@ class MedplumClient extends EventTarget {
|
|
|
166
151
|
*/
|
|
167
152
|
invalidateUrl(url) {
|
|
168
153
|
url = url.toString();
|
|
169
|
-
|
|
154
|
+
this.requestCache?.delete(url);
|
|
170
155
|
}
|
|
171
156
|
/**
|
|
172
157
|
* Invalidates all cached search results or cached requests for the given resourceType.
|
|
@@ -175,9 +160,11 @@ class MedplumClient extends EventTarget {
|
|
|
175
160
|
*/
|
|
176
161
|
invalidateSearches(resourceType) {
|
|
177
162
|
const url = 'fhir/R4/' + resourceType;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
163
|
+
if (this.requestCache) {
|
|
164
|
+
for (const key of this.requestCache.keys()) {
|
|
165
|
+
if (key.endsWith(url) || key.includes(url + '?')) {
|
|
166
|
+
this.requestCache.delete(key);
|
|
167
|
+
}
|
|
181
168
|
}
|
|
182
169
|
}
|
|
183
170
|
}
|
|
@@ -195,30 +182,30 @@ class MedplumClient extends EventTarget {
|
|
|
195
182
|
*/
|
|
196
183
|
get(url, options = {}) {
|
|
197
184
|
url = url.toString();
|
|
198
|
-
const cached =
|
|
185
|
+
const cached = this.getCacheEntry(url, options);
|
|
199
186
|
if (cached) {
|
|
200
187
|
return cached.value;
|
|
201
188
|
}
|
|
202
189
|
let promise;
|
|
203
|
-
if (url.startsWith(
|
|
190
|
+
if (url.startsWith(this.fhirBaseUrl) && this.autoBatchQueue) {
|
|
204
191
|
promise = new Promise((resolve, reject) => {
|
|
205
|
-
|
|
192
|
+
this.autoBatchQueue.push({
|
|
206
193
|
method: 'GET',
|
|
207
|
-
url: url.replace(
|
|
194
|
+
url: url.replace(this.fhirBaseUrl, ''),
|
|
208
195
|
options,
|
|
209
196
|
resolve,
|
|
210
197
|
reject,
|
|
211
198
|
});
|
|
212
|
-
if (!
|
|
213
|
-
|
|
199
|
+
if (!this.autoBatchTimerId) {
|
|
200
|
+
this.autoBatchTimerId = setTimeout(() => this.executeAutoBatch(), this.autoBatchTime);
|
|
214
201
|
}
|
|
215
202
|
});
|
|
216
203
|
}
|
|
217
204
|
else {
|
|
218
|
-
promise =
|
|
205
|
+
promise = this.request('GET', url, options);
|
|
219
206
|
}
|
|
220
207
|
const readablePromise = new ReadablePromise(promise);
|
|
221
|
-
|
|
208
|
+
this.setCacheEntry(url, readablePromise);
|
|
222
209
|
return readablePromise;
|
|
223
210
|
}
|
|
224
211
|
/**
|
|
@@ -238,13 +225,13 @@ class MedplumClient extends EventTarget {
|
|
|
238
225
|
post(url, body, contentType, options = {}) {
|
|
239
226
|
url = url.toString();
|
|
240
227
|
if (body) {
|
|
241
|
-
|
|
228
|
+
this.setRequestBody(options, body);
|
|
242
229
|
}
|
|
243
230
|
if (contentType) {
|
|
244
|
-
|
|
231
|
+
this.setRequestContentType(options, contentType);
|
|
245
232
|
}
|
|
246
233
|
this.invalidateUrl(url);
|
|
247
|
-
return
|
|
234
|
+
return this.request('POST', url, options);
|
|
248
235
|
}
|
|
249
236
|
/**
|
|
250
237
|
* Makes an HTTP PUT request to the specified URL.
|
|
@@ -263,13 +250,13 @@ class MedplumClient extends EventTarget {
|
|
|
263
250
|
put(url, body, contentType, options = {}) {
|
|
264
251
|
url = url.toString();
|
|
265
252
|
if (body) {
|
|
266
|
-
|
|
253
|
+
this.setRequestBody(options, body);
|
|
267
254
|
}
|
|
268
255
|
if (contentType) {
|
|
269
|
-
|
|
256
|
+
this.setRequestContentType(options, contentType);
|
|
270
257
|
}
|
|
271
258
|
this.invalidateUrl(url);
|
|
272
|
-
return
|
|
259
|
+
return this.request('PUT', url, options);
|
|
273
260
|
}
|
|
274
261
|
/**
|
|
275
262
|
* Makes an HTTP PATCH request to the specified URL.
|
|
@@ -286,10 +273,10 @@ class MedplumClient extends EventTarget {
|
|
|
286
273
|
*/
|
|
287
274
|
patch(url, operations, options = {}) {
|
|
288
275
|
url = url.toString();
|
|
289
|
-
|
|
290
|
-
|
|
276
|
+
this.setRequestBody(options, operations);
|
|
277
|
+
this.setRequestContentType(options, PATCH_CONTENT_TYPE);
|
|
291
278
|
this.invalidateUrl(url);
|
|
292
|
-
return
|
|
279
|
+
return this.request('PATCH', url, options);
|
|
293
280
|
}
|
|
294
281
|
/**
|
|
295
282
|
* Makes an HTTP DELETE request to the specified URL.
|
|
@@ -307,7 +294,7 @@ class MedplumClient extends EventTarget {
|
|
|
307
294
|
delete(url, options = {}) {
|
|
308
295
|
url = url.toString();
|
|
309
296
|
this.invalidateUrl(url);
|
|
310
|
-
return
|
|
297
|
+
return this.request('DELETE', url, options);
|
|
311
298
|
}
|
|
312
299
|
/**
|
|
313
300
|
* Initiates a new user flow.
|
|
@@ -359,7 +346,7 @@ class MedplumClient extends EventTarget {
|
|
|
359
346
|
async startLogin(loginRequest) {
|
|
360
347
|
return this.post('auth/login', {
|
|
361
348
|
...(await this.ensureCodeChallenge(loginRequest)),
|
|
362
|
-
clientId: loginRequest.clientId ??
|
|
349
|
+
clientId: loginRequest.clientId ?? this.clientId,
|
|
363
350
|
scope: loginRequest.scope,
|
|
364
351
|
});
|
|
365
352
|
}
|
|
@@ -374,7 +361,7 @@ class MedplumClient extends EventTarget {
|
|
|
374
361
|
async startGoogleLogin(loginRequest) {
|
|
375
362
|
return this.post('auth/google', {
|
|
376
363
|
...(await this.ensureCodeChallenge(loginRequest)),
|
|
377
|
-
clientId: loginRequest.clientId ??
|
|
364
|
+
clientId: loginRequest.clientId ?? this.clientId,
|
|
378
365
|
scope: loginRequest.scope,
|
|
379
366
|
});
|
|
380
367
|
}
|
|
@@ -398,7 +385,7 @@ class MedplumClient extends EventTarget {
|
|
|
398
385
|
* @category Authentication
|
|
399
386
|
*/
|
|
400
387
|
async signOut() {
|
|
401
|
-
await this.post(
|
|
388
|
+
await this.post(this.logoutUrl, {});
|
|
402
389
|
this.clear();
|
|
403
390
|
}
|
|
404
391
|
/**
|
|
@@ -412,7 +399,7 @@ class MedplumClient extends EventTarget {
|
|
|
412
399
|
const urlParams = new URLSearchParams(window.location.search);
|
|
413
400
|
const code = urlParams.get('code');
|
|
414
401
|
if (!code) {
|
|
415
|
-
await
|
|
402
|
+
await this.requestAuthorization(loginParams);
|
|
416
403
|
return undefined;
|
|
417
404
|
}
|
|
418
405
|
else {
|
|
@@ -425,7 +412,7 @@ class MedplumClient extends EventTarget {
|
|
|
425
412
|
* @category Authentication
|
|
426
413
|
*/
|
|
427
414
|
signOutWithRedirect() {
|
|
428
|
-
window.location.assign(
|
|
415
|
+
window.location.assign(this.logoutUrl);
|
|
429
416
|
}
|
|
430
417
|
/**
|
|
431
418
|
* Initiates sign in with an external identity provider.
|
|
@@ -446,15 +433,15 @@ class MedplumClient extends EventTarget {
|
|
|
446
433
|
* @category Authentication
|
|
447
434
|
*/
|
|
448
435
|
async exchangeExternalAccessToken(token, clientId) {
|
|
449
|
-
clientId = clientId ||
|
|
436
|
+
clientId = clientId || this.clientId;
|
|
450
437
|
if (!clientId) {
|
|
451
438
|
throw new Error('MedplumClient is missing clientId');
|
|
452
439
|
}
|
|
453
|
-
const response = await
|
|
440
|
+
const response = await this.fetch(this.exchangeUrl, {
|
|
454
441
|
method: 'POST',
|
|
455
442
|
headers: { 'Content-Type': 'application/json' },
|
|
456
443
|
body: JSON.stringify({
|
|
457
|
-
clientId:
|
|
444
|
+
clientId: this.clientId,
|
|
458
445
|
externalAccessToken: token,
|
|
459
446
|
}),
|
|
460
447
|
credentials: 'include',
|
|
@@ -464,7 +451,7 @@ class MedplumClient extends EventTarget {
|
|
|
464
451
|
throw new Error('Failed to fetch tokens');
|
|
465
452
|
}
|
|
466
453
|
const tokens = await response.json();
|
|
467
|
-
await
|
|
454
|
+
await this.verifyTokens(tokens);
|
|
468
455
|
return this.getProfile();
|
|
469
456
|
}
|
|
470
457
|
/**
|
|
@@ -493,7 +480,7 @@ class MedplumClient extends EventTarget {
|
|
|
493
480
|
* @returns The well-formed FHIR URL.
|
|
494
481
|
*/
|
|
495
482
|
fhirUrl(...path) {
|
|
496
|
-
return new URL(
|
|
483
|
+
return new URL(this.fhirBaseUrl + path.join('/'));
|
|
497
484
|
}
|
|
498
485
|
/**
|
|
499
486
|
* Builds a FHIR search URL from a search query or structured query object.
|
|
@@ -561,7 +548,7 @@ class MedplumClient extends EventTarget {
|
|
|
561
548
|
search(resourceType, query, options = {}) {
|
|
562
549
|
const url = this.fhirSearchUrl(resourceType, query);
|
|
563
550
|
const cacheKey = url.toString() + '-search';
|
|
564
|
-
const cached =
|
|
551
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
565
552
|
if (cached) {
|
|
566
553
|
return cached.value;
|
|
567
554
|
}
|
|
@@ -569,12 +556,12 @@ class MedplumClient extends EventTarget {
|
|
|
569
556
|
const bundle = await this.get(url, options);
|
|
570
557
|
if (bundle.entry) {
|
|
571
558
|
for (const entry of bundle.entry) {
|
|
572
|
-
|
|
559
|
+
this.cacheResource(entry.resource);
|
|
573
560
|
}
|
|
574
561
|
}
|
|
575
562
|
return bundle;
|
|
576
563
|
})());
|
|
577
|
-
|
|
564
|
+
this.setCacheEntry(cacheKey, promise);
|
|
578
565
|
return promise;
|
|
579
566
|
}
|
|
580
567
|
/**
|
|
@@ -604,12 +591,12 @@ class MedplumClient extends EventTarget {
|
|
|
604
591
|
url.searchParams.set('_count', '1');
|
|
605
592
|
url.searchParams.sort();
|
|
606
593
|
const cacheKey = url.toString() + '-searchOne';
|
|
607
|
-
const cached =
|
|
594
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
608
595
|
if (cached) {
|
|
609
596
|
return cached.value;
|
|
610
597
|
}
|
|
611
598
|
const promise = new ReadablePromise(this.search(resourceType, url.searchParams, options).then((b) => b.entry?.[0]?.resource));
|
|
612
|
-
|
|
599
|
+
this.setCacheEntry(cacheKey, promise);
|
|
613
600
|
return promise;
|
|
614
601
|
}
|
|
615
602
|
/**
|
|
@@ -637,12 +624,12 @@ class MedplumClient extends EventTarget {
|
|
|
637
624
|
searchResources(resourceType, query, options = {}) {
|
|
638
625
|
const url = this.fhirSearchUrl(resourceType, query);
|
|
639
626
|
const cacheKey = url.toString() + '-searchResources';
|
|
640
|
-
const cached =
|
|
627
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
641
628
|
if (cached) {
|
|
642
629
|
return cached.value;
|
|
643
630
|
}
|
|
644
631
|
const promise = new ReadablePromise(this.search(resourceType, query, options).then((b) => b.entry?.map((e) => e.resource) ?? []));
|
|
645
|
-
|
|
632
|
+
this.setCacheEntry(cacheKey, promise);
|
|
646
633
|
return promise;
|
|
647
634
|
}
|
|
648
635
|
/**
|
|
@@ -703,7 +690,7 @@ class MedplumClient extends EventTarget {
|
|
|
703
690
|
* @returns The resource if it is available in the cache; undefined otherwise.
|
|
704
691
|
*/
|
|
705
692
|
getCached(resourceType, id) {
|
|
706
|
-
const cached =
|
|
693
|
+
const cached = this.requestCache?.get(this.fhirUrl(resourceType, id).toString())?.value;
|
|
707
694
|
return cached && cached.isOk() ? cached.read() : undefined;
|
|
708
695
|
}
|
|
709
696
|
/**
|
|
@@ -805,7 +792,7 @@ class MedplumClient extends EventTarget {
|
|
|
805
792
|
return Promise.resolve(globalSchema);
|
|
806
793
|
}
|
|
807
794
|
const cacheKey = resourceType + '-requestSchema';
|
|
808
|
-
const cached =
|
|
795
|
+
const cached = this.getCacheEntry(cacheKey, undefined);
|
|
809
796
|
if (cached) {
|
|
810
797
|
return cached.value;
|
|
811
798
|
}
|
|
@@ -848,7 +835,7 @@ class MedplumClient extends EventTarget {
|
|
|
848
835
|
}
|
|
849
836
|
return globalSchema;
|
|
850
837
|
})());
|
|
851
|
-
|
|
838
|
+
this.setCacheEntry(cacheKey, promise);
|
|
852
839
|
return promise;
|
|
853
840
|
}
|
|
854
841
|
/**
|
|
@@ -1046,7 +1033,7 @@ class MedplumClient extends EventTarget {
|
|
|
1046
1033
|
};
|
|
1047
1034
|
xhr.open('POST', url);
|
|
1048
1035
|
xhr.withCredentials = true;
|
|
1049
|
-
xhr.setRequestHeader('Authorization', 'Bearer ' +
|
|
1036
|
+
xhr.setRequestHeader('Authorization', 'Bearer ' + this.accessToken);
|
|
1050
1037
|
xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, max-age=0');
|
|
1051
1038
|
xhr.setRequestHeader('Content-Type', contentType);
|
|
1052
1039
|
xhr.setRequestHeader('X-Medplum', 'extended');
|
|
@@ -1076,10 +1063,10 @@ class MedplumClient extends EventTarget {
|
|
|
1076
1063
|
* @returns The result of the create operation.
|
|
1077
1064
|
*/
|
|
1078
1065
|
async createPdf(docDefinition, filename, tableLayouts, fonts) {
|
|
1079
|
-
if (!
|
|
1066
|
+
if (!this.createPdfImpl) {
|
|
1080
1067
|
throw new Error('PDF creation not enabled');
|
|
1081
1068
|
}
|
|
1082
|
-
const blob = await
|
|
1069
|
+
const blob = await this.createPdfImpl(docDefinition, tableLayouts, fonts);
|
|
1083
1070
|
return this.createBinary(blob, filename, 'application/pdf');
|
|
1084
1071
|
}
|
|
1085
1072
|
/**
|
|
@@ -1157,7 +1144,7 @@ class MedplumClient extends EventTarget {
|
|
|
1157
1144
|
// return result ?? resource;
|
|
1158
1145
|
result = resource;
|
|
1159
1146
|
}
|
|
1160
|
-
|
|
1147
|
+
this.cacheResource(result);
|
|
1161
1148
|
return result;
|
|
1162
1149
|
}
|
|
1163
1150
|
/**
|
|
@@ -1205,7 +1192,7 @@ class MedplumClient extends EventTarget {
|
|
|
1205
1192
|
* @returns The result of the delete operation.
|
|
1206
1193
|
*/
|
|
1207
1194
|
deleteResource(resourceType, id) {
|
|
1208
|
-
|
|
1195
|
+
this.deleteCacheEntry(this.fhirUrl(resourceType, id).toString());
|
|
1209
1196
|
this.invalidateSearches(resourceType);
|
|
1210
1197
|
return this.delete(this.fhirUrl(resourceType, id));
|
|
1211
1198
|
}
|
|
@@ -1409,61 +1396,80 @@ class MedplumClient extends EventTarget {
|
|
|
1409
1396
|
* @returns The Login State
|
|
1410
1397
|
*/
|
|
1411
1398
|
getActiveLogin() {
|
|
1412
|
-
return
|
|
1399
|
+
return this.storage.getObject('activeLogin');
|
|
1413
1400
|
}
|
|
1414
1401
|
/**
|
|
1415
1402
|
* @category Authentication
|
|
1416
1403
|
*/
|
|
1417
1404
|
async setActiveLogin(login) {
|
|
1418
1405
|
this.clearActiveLogin();
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
await
|
|
1406
|
+
this.accessToken = login.accessToken;
|
|
1407
|
+
this.refreshToken = login.refreshToken;
|
|
1408
|
+
this.storage.setObject('activeLogin', login);
|
|
1409
|
+
this.addLogin(login);
|
|
1410
|
+
this.refreshPromise = undefined;
|
|
1411
|
+
await this.refreshProfile();
|
|
1425
1412
|
}
|
|
1426
1413
|
/**
|
|
1427
1414
|
* Returns the current access token.
|
|
1428
1415
|
* @category Authentication
|
|
1429
1416
|
*/
|
|
1430
1417
|
getAccessToken() {
|
|
1431
|
-
return
|
|
1418
|
+
return this.accessToken;
|
|
1432
1419
|
}
|
|
1433
1420
|
/**
|
|
1434
1421
|
* Sets the current access token.
|
|
1435
1422
|
* @category Authentication
|
|
1436
1423
|
*/
|
|
1437
1424
|
setAccessToken(accessToken) {
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1425
|
+
this.accessToken = accessToken;
|
|
1426
|
+
this.refreshToken = undefined;
|
|
1427
|
+
this.profile = undefined;
|
|
1428
|
+
this.config = undefined;
|
|
1442
1429
|
}
|
|
1443
1430
|
/**
|
|
1444
1431
|
* @category Authentication
|
|
1445
1432
|
*/
|
|
1446
1433
|
getLogins() {
|
|
1447
|
-
return
|
|
1434
|
+
return this.storage.getObject('logins') ?? [];
|
|
1435
|
+
}
|
|
1436
|
+
addLogin(newLogin) {
|
|
1437
|
+
const logins = this.getLogins().filter((login) => login.profile?.reference !== newLogin.profile?.reference);
|
|
1438
|
+
logins.push(newLogin);
|
|
1439
|
+
this.storage.setObject('logins', logins);
|
|
1440
|
+
}
|
|
1441
|
+
async refreshProfile() {
|
|
1442
|
+
this.profilePromise = new Promise((resolve, reject) => {
|
|
1443
|
+
this.get('auth/me')
|
|
1444
|
+
.then((result) => {
|
|
1445
|
+
this.profilePromise = undefined;
|
|
1446
|
+
this.profile = result.profile;
|
|
1447
|
+
this.config = result.config;
|
|
1448
|
+
this.dispatchEvent({ type: 'change' });
|
|
1449
|
+
resolve(this.profile);
|
|
1450
|
+
})
|
|
1451
|
+
.catch(reject);
|
|
1452
|
+
});
|
|
1453
|
+
return this.profilePromise;
|
|
1448
1454
|
}
|
|
1449
1455
|
/**
|
|
1450
1456
|
* @category Authentication
|
|
1451
1457
|
*/
|
|
1452
1458
|
isLoading() {
|
|
1453
|
-
return !!
|
|
1459
|
+
return !!this.profilePromise;
|
|
1454
1460
|
}
|
|
1455
1461
|
/**
|
|
1456
1462
|
* @category User Profile
|
|
1457
1463
|
*/
|
|
1458
1464
|
getProfile() {
|
|
1459
|
-
return
|
|
1465
|
+
return this.profile;
|
|
1460
1466
|
}
|
|
1461
1467
|
/**
|
|
1462
1468
|
* @category User Profile
|
|
1463
1469
|
*/
|
|
1464
1470
|
async getProfileAsync() {
|
|
1465
|
-
if (
|
|
1466
|
-
await
|
|
1471
|
+
if (this.profilePromise) {
|
|
1472
|
+
await this.profilePromise;
|
|
1467
1473
|
}
|
|
1468
1474
|
return this.getProfile();
|
|
1469
1475
|
}
|
|
@@ -1471,7 +1477,7 @@ class MedplumClient extends EventTarget {
|
|
|
1471
1477
|
* @category User Profile
|
|
1472
1478
|
*/
|
|
1473
1479
|
getUserConfiguration() {
|
|
1474
|
-
return
|
|
1480
|
+
return this.config;
|
|
1475
1481
|
}
|
|
1476
1482
|
/**
|
|
1477
1483
|
* Downloads the URL as a blob.
|
|
@@ -1481,13 +1487,234 @@ class MedplumClient extends EventTarget {
|
|
|
1481
1487
|
* @returns Promise to the response body as a blob.
|
|
1482
1488
|
*/
|
|
1483
1489
|
async download(url, options = {}) {
|
|
1484
|
-
if (
|
|
1485
|
-
await
|
|
1490
|
+
if (this.refreshPromise) {
|
|
1491
|
+
await this.refreshPromise;
|
|
1486
1492
|
}
|
|
1487
|
-
|
|
1488
|
-
const response = await
|
|
1493
|
+
this.addFetchOptionsDefaults(options);
|
|
1494
|
+
const response = await this.fetch(url.toString(), options);
|
|
1489
1495
|
return response.blob();
|
|
1490
1496
|
}
|
|
1497
|
+
//
|
|
1498
|
+
// Private helpers
|
|
1499
|
+
//
|
|
1500
|
+
/**
|
|
1501
|
+
* Returns the cache entry if available and not expired.
|
|
1502
|
+
* @param key The cache key to retrieve.
|
|
1503
|
+
* @param options Optional fetch options for cache settings.
|
|
1504
|
+
* @returns The cached entry if found.
|
|
1505
|
+
*/
|
|
1506
|
+
getCacheEntry(key, options) {
|
|
1507
|
+
if (!this.requestCache || options?.cache === 'no-cache' || options?.cache === 'reload') {
|
|
1508
|
+
return undefined;
|
|
1509
|
+
}
|
|
1510
|
+
const entry = this.requestCache.get(key);
|
|
1511
|
+
if (!entry || entry.requestTime + this.cacheTime < Date.now()) {
|
|
1512
|
+
return undefined;
|
|
1513
|
+
}
|
|
1514
|
+
return entry;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Adds a readable promise to the cache.
|
|
1518
|
+
* @param key The cache key to store.
|
|
1519
|
+
* @param value The readable promise to store.
|
|
1520
|
+
*/
|
|
1521
|
+
setCacheEntry(key, value) {
|
|
1522
|
+
if (this.requestCache) {
|
|
1523
|
+
this.requestCache.set(key, { requestTime: Date.now(), value });
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Adds a concrete value as the cache entry for the given resource.
|
|
1528
|
+
* This is used in cases where the resource is loaded indirectly.
|
|
1529
|
+
* For example, when a resource is loaded as part of a Bundle.
|
|
1530
|
+
* @param resource The resource to cache.
|
|
1531
|
+
*/
|
|
1532
|
+
cacheResource(resource) {
|
|
1533
|
+
if (resource?.id) {
|
|
1534
|
+
this.setCacheEntry(this.fhirUrl(resource.resourceType, resource.id).toString(), new ReadablePromise(Promise.resolve(resource)));
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Deletes a cache entry.
|
|
1539
|
+
* @param key The cache key to delete.
|
|
1540
|
+
*/
|
|
1541
|
+
deleteCacheEntry(key) {
|
|
1542
|
+
if (this.requestCache) {
|
|
1543
|
+
this.requestCache.delete(key);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Makes an HTTP request.
|
|
1548
|
+
* @param {string} method
|
|
1549
|
+
* @param {string} url
|
|
1550
|
+
* @param {string=} contentType
|
|
1551
|
+
* @param {Object=} body
|
|
1552
|
+
*/
|
|
1553
|
+
async request(method, url, options = {}) {
|
|
1554
|
+
if (this.refreshPromise) {
|
|
1555
|
+
await this.refreshPromise;
|
|
1556
|
+
}
|
|
1557
|
+
if (!url.startsWith('http')) {
|
|
1558
|
+
url = this.baseUrl + url;
|
|
1559
|
+
}
|
|
1560
|
+
options.method = method;
|
|
1561
|
+
this.addFetchOptionsDefaults(options);
|
|
1562
|
+
const response = await this.fetchWithRetry(url, options);
|
|
1563
|
+
if (response.status === 401) {
|
|
1564
|
+
// Refresh and try again
|
|
1565
|
+
return this.handleUnauthenticated(method, url, options);
|
|
1566
|
+
}
|
|
1567
|
+
if (response.status === 204 || response.status === 304) {
|
|
1568
|
+
// No content or change
|
|
1569
|
+
return undefined;
|
|
1570
|
+
}
|
|
1571
|
+
let obj = undefined;
|
|
1572
|
+
try {
|
|
1573
|
+
obj = await response.json();
|
|
1574
|
+
}
|
|
1575
|
+
catch (err) {
|
|
1576
|
+
console.error('Error parsing response', response.status, err);
|
|
1577
|
+
throw err;
|
|
1578
|
+
}
|
|
1579
|
+
if (response.status >= 400) {
|
|
1580
|
+
throw new OperationOutcomeError(normalizeOperationOutcome(obj));
|
|
1581
|
+
}
|
|
1582
|
+
return obj;
|
|
1583
|
+
}
|
|
1584
|
+
async fetchWithRetry(url, options) {
|
|
1585
|
+
const maxRetries = 3;
|
|
1586
|
+
const retryDelay = 200;
|
|
1587
|
+
let response = undefined;
|
|
1588
|
+
for (let retry = 0; retry < maxRetries; retry++) {
|
|
1589
|
+
response = (await this.fetch(url, options));
|
|
1590
|
+
if (response.status < 500) {
|
|
1591
|
+
return response;
|
|
1592
|
+
}
|
|
1593
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1594
|
+
}
|
|
1595
|
+
return response;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Executes a batch of requests that were automatically batched together.
|
|
1599
|
+
*/
|
|
1600
|
+
async executeAutoBatch() {
|
|
1601
|
+
// Get the current queue
|
|
1602
|
+
const entries = [...this.autoBatchQueue];
|
|
1603
|
+
// Clear the queue
|
|
1604
|
+
this.autoBatchQueue.length = 0;
|
|
1605
|
+
// Clear the timer
|
|
1606
|
+
this.autoBatchTimerId = undefined;
|
|
1607
|
+
// If there is only one request in the batch, just execute it
|
|
1608
|
+
if (entries.length === 1) {
|
|
1609
|
+
const entry = entries[0];
|
|
1610
|
+
try {
|
|
1611
|
+
entry.resolve(await this.request(entry.method, this.fhirBaseUrl + entry.url, entry.options));
|
|
1612
|
+
}
|
|
1613
|
+
catch (err) {
|
|
1614
|
+
entry.reject(new OperationOutcomeError(normalizeOperationOutcome(err)));
|
|
1615
|
+
}
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
// Build the batch request
|
|
1619
|
+
const batch = {
|
|
1620
|
+
resourceType: 'Bundle',
|
|
1621
|
+
type: 'batch',
|
|
1622
|
+
entry: entries.map((e) => ({
|
|
1623
|
+
request: {
|
|
1624
|
+
method: e.method,
|
|
1625
|
+
url: e.url,
|
|
1626
|
+
},
|
|
1627
|
+
resource: e.options.body ? JSON.parse(e.options.body) : undefined,
|
|
1628
|
+
})),
|
|
1629
|
+
};
|
|
1630
|
+
// Execute the batch request
|
|
1631
|
+
const response = (await this.post('fhir/R4', batch));
|
|
1632
|
+
// Process the response
|
|
1633
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1634
|
+
const entry = entries[i];
|
|
1635
|
+
const responseEntry = response.entry?.[i];
|
|
1636
|
+
if (responseEntry?.response?.outcome && !isOk(responseEntry.response.outcome)) {
|
|
1637
|
+
entry.reject(new OperationOutcomeError(responseEntry.response.outcome));
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
entry.resolve(responseEntry?.resource);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Adds default options to the fetch options.
|
|
1646
|
+
* @param options The options to add defaults to.
|
|
1647
|
+
*/
|
|
1648
|
+
addFetchOptionsDefaults(options) {
|
|
1649
|
+
let headers = options.headers;
|
|
1650
|
+
if (!headers) {
|
|
1651
|
+
headers = {};
|
|
1652
|
+
options.headers = headers;
|
|
1653
|
+
}
|
|
1654
|
+
headers['X-Medplum'] = 'extended';
|
|
1655
|
+
if (options.body && !headers['Content-Type']) {
|
|
1656
|
+
headers['Content-Type'] = FHIR_CONTENT_TYPE;
|
|
1657
|
+
}
|
|
1658
|
+
if (this.accessToken) {
|
|
1659
|
+
headers['Authorization'] = 'Bearer ' + this.accessToken;
|
|
1660
|
+
}
|
|
1661
|
+
if (this.basicAuth) {
|
|
1662
|
+
headers['Authorization'] = 'Basic ' + this.basicAuth;
|
|
1663
|
+
}
|
|
1664
|
+
if (!options.cache) {
|
|
1665
|
+
options.cache = 'no-cache';
|
|
1666
|
+
}
|
|
1667
|
+
if (!options.credentials) {
|
|
1668
|
+
options.credentials = 'include';
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Sets the "Content-Type" header on fetch options.
|
|
1673
|
+
* @param options The fetch options.
|
|
1674
|
+
* @param contentType The new content type to set.
|
|
1675
|
+
*/
|
|
1676
|
+
setRequestContentType(options, contentType) {
|
|
1677
|
+
if (!options.headers) {
|
|
1678
|
+
options.headers = {};
|
|
1679
|
+
}
|
|
1680
|
+
const headers = options.headers;
|
|
1681
|
+
headers['Content-Type'] = contentType;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Sets the body on fetch options.
|
|
1685
|
+
* @param options The fetch options.
|
|
1686
|
+
* @param data The new content body.
|
|
1687
|
+
*/
|
|
1688
|
+
setRequestBody(options, data) {
|
|
1689
|
+
if (typeof data === 'string' ||
|
|
1690
|
+
(typeof Blob !== 'undefined' && data instanceof Blob) ||
|
|
1691
|
+
(typeof File !== 'undefined' && data instanceof File) ||
|
|
1692
|
+
(typeof Uint8Array !== 'undefined' && data instanceof Uint8Array)) {
|
|
1693
|
+
options.body = data;
|
|
1694
|
+
}
|
|
1695
|
+
else if (data) {
|
|
1696
|
+
options.body = JSON.stringify(data);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Handles an unauthenticated response from the server.
|
|
1701
|
+
* First, tries to refresh the access token and retry the request.
|
|
1702
|
+
* Otherwise, calls unauthenticated callbacks and rejects.
|
|
1703
|
+
* @param method The HTTP method of the original request.
|
|
1704
|
+
* @param url The URL of the original request.
|
|
1705
|
+
* @param contentType The content type of the original request.
|
|
1706
|
+
* @param body The body of the original request.
|
|
1707
|
+
*/
|
|
1708
|
+
handleUnauthenticated(method, url, options) {
|
|
1709
|
+
if (this.refresh()) {
|
|
1710
|
+
return this.request(method, url, options);
|
|
1711
|
+
}
|
|
1712
|
+
this.clearActiveLogin();
|
|
1713
|
+
if (this.onUnauthenticated) {
|
|
1714
|
+
this.onUnauthenticated();
|
|
1715
|
+
}
|
|
1716
|
+
return Promise.reject(new Error('Unauthenticated'));
|
|
1717
|
+
}
|
|
1491
1718
|
/**
|
|
1492
1719
|
* Starts a new PKCE flow.
|
|
1493
1720
|
* These PKCE values are stateful, and must survive redirects and page refreshes.
|
|
@@ -1503,6 +1730,23 @@ class MedplumClient extends EventTarget {
|
|
|
1503
1730
|
sessionStorage.setItem('codeChallenge', codeChallenge);
|
|
1504
1731
|
return { codeChallengeMethod: 'S256', codeChallenge };
|
|
1505
1732
|
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Redirects the user to the login screen for authorization.
|
|
1735
|
+
* Clears all auth state including local storage and session storage.
|
|
1736
|
+
* See: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
|
1737
|
+
*/
|
|
1738
|
+
async requestAuthorization(loginParams) {
|
|
1739
|
+
const loginRequest = await this.ensureCodeChallenge(loginParams || {});
|
|
1740
|
+
const url = new URL(this.authorizeUrl);
|
|
1741
|
+
url.searchParams.set('response_type', 'code');
|
|
1742
|
+
url.searchParams.set('state', sessionStorage.getItem('pkceState'));
|
|
1743
|
+
url.searchParams.set('client_id', loginRequest.clientId || this.clientId);
|
|
1744
|
+
url.searchParams.set('redirect_uri', loginRequest.redirectUri || getWindowOrigin());
|
|
1745
|
+
url.searchParams.set('code_challenge_method', loginRequest.codeChallengeMethod);
|
|
1746
|
+
url.searchParams.set('code_challenge', loginRequest.codeChallenge);
|
|
1747
|
+
url.searchParams.set('scope', loginRequest.scope || 'openid profile');
|
|
1748
|
+
window.location.assign(url.toString());
|
|
1749
|
+
}
|
|
1506
1750
|
/**
|
|
1507
1751
|
* Processes an OAuth authorization code.
|
|
1508
1752
|
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
|
@@ -1514,7 +1758,7 @@ class MedplumClient extends EventTarget {
|
|
|
1514
1758
|
const formBody = new URLSearchParams();
|
|
1515
1759
|
formBody.set('grant_type', 'authorization_code');
|
|
1516
1760
|
formBody.set('code', code);
|
|
1517
|
-
formBody.set('client_id', loginParams?.clientId ||
|
|
1761
|
+
formBody.set('client_id', loginParams?.clientId || this.clientId);
|
|
1518
1762
|
formBody.set('redirect_uri', loginParams?.redirectUri || getWindowOrigin());
|
|
1519
1763
|
if (typeof sessionStorage !== 'undefined') {
|
|
1520
1764
|
const codeVerifier = sessionStorage.getItem('codeVerifier');
|
|
@@ -1522,7 +1766,29 @@ class MedplumClient extends EventTarget {
|
|
|
1522
1766
|
formBody.set('code_verifier', codeVerifier);
|
|
1523
1767
|
}
|
|
1524
1768
|
}
|
|
1525
|
-
return
|
|
1769
|
+
return this.fetchTokens(formBody);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Tries to refresh the auth tokens.
|
|
1773
|
+
* See: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
|
1774
|
+
*/
|
|
1775
|
+
refresh() {
|
|
1776
|
+
if (this.refreshPromise) {
|
|
1777
|
+
return this.refreshPromise;
|
|
1778
|
+
}
|
|
1779
|
+
if (this.refreshToken) {
|
|
1780
|
+
const formBody = new URLSearchParams();
|
|
1781
|
+
formBody.set('grant_type', 'refresh_token');
|
|
1782
|
+
formBody.set('client_id', this.clientId);
|
|
1783
|
+
formBody.set('refresh_token', this.refreshToken);
|
|
1784
|
+
this.refreshPromise = this.fetchTokens(formBody);
|
|
1785
|
+
return this.refreshPromise;
|
|
1786
|
+
}
|
|
1787
|
+
if (this.clientId && this.clientSecret) {
|
|
1788
|
+
this.refreshPromise = this.startClientLogin(this.clientId, this.clientSecret);
|
|
1789
|
+
return this.refreshPromise;
|
|
1790
|
+
}
|
|
1791
|
+
return undefined;
|
|
1526
1792
|
}
|
|
1527
1793
|
/**
|
|
1528
1794
|
* Starts a new OAuth2 client credentials flow.
|
|
@@ -1533,13 +1799,24 @@ class MedplumClient extends EventTarget {
|
|
|
1533
1799
|
* @returns Promise that resolves to the client profile.
|
|
1534
1800
|
*/
|
|
1535
1801
|
async startClientLogin(clientId, clientSecret) {
|
|
1536
|
-
|
|
1537
|
-
|
|
1802
|
+
this.clientId = clientId;
|
|
1803
|
+
this.clientSecret = clientSecret;
|
|
1538
1804
|
const formBody = new URLSearchParams();
|
|
1539
1805
|
formBody.set('grant_type', 'client_credentials');
|
|
1540
1806
|
formBody.set('client_id', clientId);
|
|
1541
1807
|
formBody.set('client_secret', clientSecret);
|
|
1542
|
-
return
|
|
1808
|
+
return this.fetchTokens(formBody);
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Sets the client ID and secret for basic auth.
|
|
1812
|
+
* @category Authentication
|
|
1813
|
+
* @param clientId The client ID.
|
|
1814
|
+
* @param clientSecret The client secret.
|
|
1815
|
+
*/
|
|
1816
|
+
setBasicAuth(clientId, clientSecret) {
|
|
1817
|
+
this.clientId = clientId;
|
|
1818
|
+
this.clientSecret = clientSecret;
|
|
1819
|
+
this.basicAuth = encodeBase64(clientId + ':' + clientSecret);
|
|
1543
1820
|
}
|
|
1544
1821
|
/**
|
|
1545
1822
|
* Invite a user to a project.
|
|
@@ -1550,280 +1827,72 @@ class MedplumClient extends EventTarget {
|
|
|
1550
1827
|
async invite(projectId, body) {
|
|
1551
1828
|
return this.post('admin/projects/' + projectId + '/invite', body);
|
|
1552
1829
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
if (__classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") <= 0 || options?.cache === 'no-cache' || options?.cache === 'reload') {
|
|
1573
|
-
return undefined;
|
|
1574
|
-
}
|
|
1575
|
-
const entry = __classPrivateFieldGet(this, _MedplumClient_requestCache, "f").get(key);
|
|
1576
|
-
if (!entry || entry.requestTime + __classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") < Date.now()) {
|
|
1577
|
-
return undefined;
|
|
1578
|
-
}
|
|
1579
|
-
return entry;
|
|
1580
|
-
}, _MedplumClient_setCacheEntry = function _MedplumClient_setCacheEntry(key, value) {
|
|
1581
|
-
if (__classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") > 0) {
|
|
1582
|
-
__classPrivateFieldGet(this, _MedplumClient_requestCache, "f").set(key, { requestTime: Date.now(), value });
|
|
1583
|
-
}
|
|
1584
|
-
}, _MedplumClient_cacheResource = function _MedplumClient_cacheResource(resource) {
|
|
1585
|
-
if (resource?.id) {
|
|
1586
|
-
__classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_setCacheEntry).call(this, this.fhirUrl(resource.resourceType, resource.id).toString(), new ReadablePromise(Promise.resolve(resource)));
|
|
1587
|
-
}
|
|
1588
|
-
}, _MedplumClient_deleteCacheEntry = function _MedplumClient_deleteCacheEntry(key) {
|
|
1589
|
-
if (__classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") > 0) {
|
|
1590
|
-
__classPrivateFieldGet(this, _MedplumClient_requestCache, "f").delete(key);
|
|
1591
|
-
}
|
|
1592
|
-
}, _MedplumClient_request =
|
|
1593
|
-
/**
|
|
1594
|
-
* Makes an HTTP request.
|
|
1595
|
-
* @param {string} method
|
|
1596
|
-
* @param {string} url
|
|
1597
|
-
* @param {string=} contentType
|
|
1598
|
-
* @param {Object=} body
|
|
1599
|
-
*/
|
|
1600
|
-
async function _MedplumClient_request(method, url, options = {}) {
|
|
1601
|
-
if (__classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f")) {
|
|
1602
|
-
await __classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f");
|
|
1603
|
-
}
|
|
1604
|
-
if (!url.startsWith('http')) {
|
|
1605
|
-
url = __classPrivateFieldGet(this, _MedplumClient_baseUrl, "f") + url;
|
|
1606
|
-
}
|
|
1607
|
-
options.method = method;
|
|
1608
|
-
__classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_addFetchOptionsDefaults).call(this, options);
|
|
1609
|
-
const response = await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_fetchWithRetry).call(this, url, options);
|
|
1610
|
-
if (response.status === 401) {
|
|
1611
|
-
// Refresh and try again
|
|
1612
|
-
return __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_handleUnauthenticated).call(this, method, url, options);
|
|
1613
|
-
}
|
|
1614
|
-
if (response.status === 204 || response.status === 304) {
|
|
1615
|
-
// No content or change
|
|
1616
|
-
return undefined;
|
|
1617
|
-
}
|
|
1618
|
-
let obj = undefined;
|
|
1619
|
-
try {
|
|
1620
|
-
obj = await response.json();
|
|
1621
|
-
}
|
|
1622
|
-
catch (err) {
|
|
1623
|
-
console.error('Error parsing response', response.status, err);
|
|
1624
|
-
throw err;
|
|
1625
|
-
}
|
|
1626
|
-
if (response.status >= 400) {
|
|
1627
|
-
throw new OperationOutcomeError(normalizeOperationOutcome(obj));
|
|
1830
|
+
/**
|
|
1831
|
+
* Makes a POST request to the tokens endpoint.
|
|
1832
|
+
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
1833
|
+
* @param formBody Token parameters in URL encoded format.
|
|
1834
|
+
*/
|
|
1835
|
+
async fetchTokens(formBody) {
|
|
1836
|
+
const response = await this.fetch(this.tokenUrl, {
|
|
1837
|
+
method: 'POST',
|
|
1838
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1839
|
+
body: formBody,
|
|
1840
|
+
credentials: 'include',
|
|
1841
|
+
});
|
|
1842
|
+
if (!response.ok) {
|
|
1843
|
+
this.clearActiveLogin();
|
|
1844
|
+
throw new Error('Failed to fetch tokens');
|
|
1845
|
+
}
|
|
1846
|
+
const tokens = await response.json();
|
|
1847
|
+
await this.verifyTokens(tokens);
|
|
1848
|
+
return this.getProfile();
|
|
1628
1849
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1850
|
+
/**
|
|
1851
|
+
* Verifies the tokens received from the auth server.
|
|
1852
|
+
* Validates the JWT against the JWKS.
|
|
1853
|
+
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
1854
|
+
* @param tokens
|
|
1855
|
+
*/
|
|
1856
|
+
async verifyTokens(tokens) {
|
|
1857
|
+
const token = tokens.access_token;
|
|
1858
|
+
// Verify token has not expired
|
|
1859
|
+
const tokenPayload = parseJWTPayload(token);
|
|
1860
|
+
if (Date.now() >= tokenPayload.exp * 1000) {
|
|
1861
|
+
this.clearActiveLogin();
|
|
1862
|
+
throw new Error('Token expired');
|
|
1638
1863
|
}
|
|
1639
|
-
|
|
1864
|
+
// Verify app_client_id
|
|
1865
|
+
if (this.clientId && tokenPayload.client_id !== this.clientId) {
|
|
1866
|
+
this.clearActiveLogin();
|
|
1867
|
+
throw new Error('Token was not issued for this audience');
|
|
1868
|
+
}
|
|
1869
|
+
return this.setActiveLogin({
|
|
1870
|
+
accessToken: token,
|
|
1871
|
+
refreshToken: tokens.refresh_token,
|
|
1872
|
+
project: tokens.project,
|
|
1873
|
+
profile: tokens.profile,
|
|
1874
|
+
});
|
|
1640
1875
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
async function _MedplumClient_executeAutoBatch() {
|
|
1647
|
-
// Get the current queue
|
|
1648
|
-
const entries = [...__classPrivateFieldGet(this, _MedplumClient_autoBatchQueue, "f")];
|
|
1649
|
-
// Clear the queue
|
|
1650
|
-
__classPrivateFieldGet(this, _MedplumClient_autoBatchQueue, "f").length = 0;
|
|
1651
|
-
// Clear the timer
|
|
1652
|
-
__classPrivateFieldSet(this, _MedplumClient_autoBatchTimerId, undefined, "f");
|
|
1653
|
-
// If there is only one request in the batch, just execute it
|
|
1654
|
-
if (entries.length === 1) {
|
|
1655
|
-
const entry = entries[0];
|
|
1876
|
+
/**
|
|
1877
|
+
* Sets up a listener for window storage events.
|
|
1878
|
+
* This synchronizes state across browser windows and browser tabs.
|
|
1879
|
+
*/
|
|
1880
|
+
setupStorageListener() {
|
|
1656
1881
|
try {
|
|
1657
|
-
|
|
1882
|
+
window.addEventListener('storage', (e) => {
|
|
1883
|
+
if (e.key === null || e.key === 'activeLogin') {
|
|
1884
|
+
// Storage events fire when different tabs make changes.
|
|
1885
|
+
// On storage clear (key === null) or activeLogin change (key === 'activeLogin')
|
|
1886
|
+
// Refresh the page to ensure the active login is up to date.
|
|
1887
|
+
window.location.reload();
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1658
1890
|
}
|
|
1659
1891
|
catch (err) {
|
|
1660
|
-
|
|
1661
|
-
}
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
// Build the batch request
|
|
1665
|
-
const batch = {
|
|
1666
|
-
resourceType: 'Bundle',
|
|
1667
|
-
type: 'batch',
|
|
1668
|
-
entry: entries.map((e) => ({
|
|
1669
|
-
request: {
|
|
1670
|
-
method: e.method,
|
|
1671
|
-
url: e.url,
|
|
1672
|
-
},
|
|
1673
|
-
resource: e.options.body ? JSON.parse(e.options.body) : undefined,
|
|
1674
|
-
})),
|
|
1675
|
-
};
|
|
1676
|
-
// Execute the batch request
|
|
1677
|
-
const response = (await this.post('fhir/R4', batch));
|
|
1678
|
-
// Process the response
|
|
1679
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1680
|
-
const entry = entries[i];
|
|
1681
|
-
const responseEntry = response.entry?.[i];
|
|
1682
|
-
if (responseEntry?.response?.outcome && !isOk(responseEntry.response.outcome)) {
|
|
1683
|
-
entry.reject(new OperationOutcomeError(responseEntry.response.outcome));
|
|
1892
|
+
// Silently ignore if this environment does not support storage events
|
|
1684
1893
|
}
|
|
1685
|
-
else {
|
|
1686
|
-
entry.resolve(responseEntry?.resource);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
}, _MedplumClient_addFetchOptionsDefaults = function _MedplumClient_addFetchOptionsDefaults(options) {
|
|
1690
|
-
if (!options.headers) {
|
|
1691
|
-
options.headers = {};
|
|
1692
|
-
}
|
|
1693
|
-
const headers = options.headers;
|
|
1694
|
-
headers['X-Medplum'] = 'extended';
|
|
1695
|
-
if (!headers['Content-Type']) {
|
|
1696
|
-
headers['Content-Type'] = FHIR_CONTENT_TYPE;
|
|
1697
|
-
}
|
|
1698
|
-
if (__classPrivateFieldGet(this, _MedplumClient_accessToken, "f")) {
|
|
1699
|
-
headers['Authorization'] = 'Bearer ' + __classPrivateFieldGet(this, _MedplumClient_accessToken, "f");
|
|
1700
|
-
}
|
|
1701
|
-
if (!options.cache) {
|
|
1702
|
-
options.cache = 'no-cache';
|
|
1703
|
-
}
|
|
1704
|
-
if (!options.credentials) {
|
|
1705
|
-
options.credentials = 'include';
|
|
1706
|
-
}
|
|
1707
|
-
}, _MedplumClient_setRequestContentType = function _MedplumClient_setRequestContentType(options, contentType) {
|
|
1708
|
-
if (!options.headers) {
|
|
1709
|
-
options.headers = {};
|
|
1710
|
-
}
|
|
1711
|
-
const headers = options.headers;
|
|
1712
|
-
headers['Content-Type'] = contentType;
|
|
1713
|
-
}, _MedplumClient_setRequestBody = function _MedplumClient_setRequestBody(options, data) {
|
|
1714
|
-
if (typeof data === 'string' ||
|
|
1715
|
-
(typeof Blob !== 'undefined' && data instanceof Blob) ||
|
|
1716
|
-
(typeof File !== 'undefined' && data instanceof File) ||
|
|
1717
|
-
(typeof Uint8Array !== 'undefined' && data instanceof Uint8Array)) {
|
|
1718
|
-
options.body = data;
|
|
1719
|
-
}
|
|
1720
|
-
else if (data) {
|
|
1721
|
-
options.body = JSON.stringify(data);
|
|
1722
1894
|
}
|
|
1723
|
-
}
|
|
1724
|
-
if (__classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_refresh).call(this)) {
|
|
1725
|
-
return __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_request).call(this, method, url, options);
|
|
1726
|
-
}
|
|
1727
|
-
this.clearActiveLogin();
|
|
1728
|
-
if (__classPrivateFieldGet(this, _MedplumClient_onUnauthenticated, "f")) {
|
|
1729
|
-
__classPrivateFieldGet(this, _MedplumClient_onUnauthenticated, "f").call(this);
|
|
1730
|
-
}
|
|
1731
|
-
return Promise.reject(new Error('Unauthenticated'));
|
|
1732
|
-
}, _MedplumClient_requestAuthorization =
|
|
1733
|
-
/**
|
|
1734
|
-
* Redirects the user to the login screen for authorization.
|
|
1735
|
-
* Clears all auth state including local storage and session storage.
|
|
1736
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
|
1737
|
-
*/
|
|
1738
|
-
async function _MedplumClient_requestAuthorization(loginParams) {
|
|
1739
|
-
const loginRequest = await this.ensureCodeChallenge(loginParams || {});
|
|
1740
|
-
const url = new URL(__classPrivateFieldGet(this, _MedplumClient_authorizeUrl, "f"));
|
|
1741
|
-
url.searchParams.set('response_type', 'code');
|
|
1742
|
-
url.searchParams.set('state', sessionStorage.getItem('pkceState'));
|
|
1743
|
-
url.searchParams.set('client_id', loginRequest.clientId || __classPrivateFieldGet(this, _MedplumClient_clientId, "f"));
|
|
1744
|
-
url.searchParams.set('redirect_uri', loginRequest.redirectUri || getWindowOrigin());
|
|
1745
|
-
url.searchParams.set('code_challenge_method', loginRequest.codeChallengeMethod);
|
|
1746
|
-
url.searchParams.set('code_challenge', loginRequest.codeChallenge);
|
|
1747
|
-
url.searchParams.set('scope', loginRequest.scope || 'openid profile');
|
|
1748
|
-
window.location.assign(url.toString());
|
|
1749
|
-
}, _MedplumClient_refresh = function _MedplumClient_refresh() {
|
|
1750
|
-
if (__classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f")) {
|
|
1751
|
-
return __classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f");
|
|
1752
|
-
}
|
|
1753
|
-
if (__classPrivateFieldGet(this, _MedplumClient_refreshToken, "f")) {
|
|
1754
|
-
const formBody = new URLSearchParams();
|
|
1755
|
-
formBody.set('grant_type', 'refresh_token');
|
|
1756
|
-
formBody.set('client_id', __classPrivateFieldGet(this, _MedplumClient_clientId, "f"));
|
|
1757
|
-
formBody.set('refresh_token', __classPrivateFieldGet(this, _MedplumClient_refreshToken, "f"));
|
|
1758
|
-
__classPrivateFieldSet(this, _MedplumClient_refreshPromise, __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_fetchTokens).call(this, formBody), "f");
|
|
1759
|
-
return __classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f");
|
|
1760
|
-
}
|
|
1761
|
-
if (__classPrivateFieldGet(this, _MedplumClient_clientId, "f") && __classPrivateFieldGet(this, _MedplumClient_clientSecret, "f")) {
|
|
1762
|
-
__classPrivateFieldSet(this, _MedplumClient_refreshPromise, this.startClientLogin(__classPrivateFieldGet(this, _MedplumClient_clientId, "f"), __classPrivateFieldGet(this, _MedplumClient_clientSecret, "f")), "f");
|
|
1763
|
-
return __classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f");
|
|
1764
|
-
}
|
|
1765
|
-
return undefined;
|
|
1766
|
-
}, _MedplumClient_fetchTokens =
|
|
1767
|
-
/**
|
|
1768
|
-
* Makes a POST request to the tokens endpoint.
|
|
1769
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
1770
|
-
* @param formBody Token parameters in URL encoded format.
|
|
1771
|
-
*/
|
|
1772
|
-
async function _MedplumClient_fetchTokens(formBody) {
|
|
1773
|
-
const response = await __classPrivateFieldGet(this, _MedplumClient_fetch, "f").call(this, __classPrivateFieldGet(this, _MedplumClient_tokenUrl, "f"), {
|
|
1774
|
-
method: 'POST',
|
|
1775
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1776
|
-
body: formBody,
|
|
1777
|
-
credentials: 'include',
|
|
1778
|
-
});
|
|
1779
|
-
if (!response.ok) {
|
|
1780
|
-
this.clearActiveLogin();
|
|
1781
|
-
throw new Error('Failed to fetch tokens');
|
|
1782
|
-
}
|
|
1783
|
-
const tokens = await response.json();
|
|
1784
|
-
await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_verifyTokens).call(this, tokens);
|
|
1785
|
-
return this.getProfile();
|
|
1786
|
-
}, _MedplumClient_verifyTokens =
|
|
1787
|
-
/**
|
|
1788
|
-
* Verifies the tokens received from the auth server.
|
|
1789
|
-
* Validates the JWT against the JWKS.
|
|
1790
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
1791
|
-
* @param tokens
|
|
1792
|
-
*/
|
|
1793
|
-
async function _MedplumClient_verifyTokens(tokens) {
|
|
1794
|
-
const token = tokens.access_token;
|
|
1795
|
-
// Verify token has not expired
|
|
1796
|
-
const tokenPayload = parseJWTPayload(token);
|
|
1797
|
-
if (Date.now() >= tokenPayload.exp * 1000) {
|
|
1798
|
-
this.clearActiveLogin();
|
|
1799
|
-
throw new Error('Token expired');
|
|
1800
|
-
}
|
|
1801
|
-
// Verify app_client_id
|
|
1802
|
-
if (__classPrivateFieldGet(this, _MedplumClient_clientId, "f") && tokenPayload.client_id !== __classPrivateFieldGet(this, _MedplumClient_clientId, "f")) {
|
|
1803
|
-
this.clearActiveLogin();
|
|
1804
|
-
throw new Error('Token was not issued for this audience');
|
|
1805
|
-
}
|
|
1806
|
-
return this.setActiveLogin({
|
|
1807
|
-
accessToken: token,
|
|
1808
|
-
refreshToken: tokens.refresh_token,
|
|
1809
|
-
project: tokens.project,
|
|
1810
|
-
profile: tokens.profile,
|
|
1811
|
-
});
|
|
1812
|
-
}, _MedplumClient_setupStorageListener = function _MedplumClient_setupStorageListener() {
|
|
1813
|
-
try {
|
|
1814
|
-
window.addEventListener('storage', (e) => {
|
|
1815
|
-
if (e.key === null || e.key === 'activeLogin') {
|
|
1816
|
-
// Storage events fire when different tabs make changes.
|
|
1817
|
-
// On storage clear (key === null) or activeLogin change (key === 'activeLogin')
|
|
1818
|
-
// Refresh the page to ensure the active login is up to date.
|
|
1819
|
-
window.location.reload();
|
|
1820
|
-
}
|
|
1821
|
-
});
|
|
1822
|
-
}
|
|
1823
|
-
catch (err) {
|
|
1824
|
-
// Silently ignore if this environment does not support storage events
|
|
1825
|
-
}
|
|
1826
|
-
};
|
|
1895
|
+
}
|
|
1827
1896
|
/**
|
|
1828
1897
|
* Returns the default fetch method.
|
|
1829
1898
|
* The default fetch is currently only available in browser environments.
|