@medplum/core 2.0.13 → 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 +1259 -1065
- 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/bundle.mjs +36 -0
- package/dist/esm/bundle.mjs.map +1 -0
- package/dist/esm/cache.mjs +17 -23
- package/dist/esm/cache.mjs.map +1 -1
- package/dist/esm/client.mjs +523 -385
- 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 +2 -4
- package/dist/esm/fhirmapper/tokenize.mjs.map +1 -1
- package/dist/esm/fhirpath/atoms.mjs +13 -20
- 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 +1 -4
- package/dist/esm/filter/parse.mjs.map +1 -1
- package/dist/esm/filter/tokenize.mjs +2 -4
- package/dist/esm/filter/tokenize.mjs.map +1 -1
- package/dist/esm/index.min.mjs +1 -1
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +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 +149 -127
- package/dist/esm/schema.mjs.map +1 -1
- package/dist/esm/search/match.mjs +1 -4
- 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/bundle.d.ts +11 -0
- package/dist/types/cache.d.ts +3 -1
- package/dist/types/client.d.ts +164 -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/fhirpath/atoms.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- 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_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.13-ee64d72c";
|
|
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,55 +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_onUnauthenticated.set(this, void 0);
|
|
90
|
-
_MedplumClient_autoBatchTime.set(this, void 0);
|
|
91
|
-
_MedplumClient_autoBatchQueue.set(this, void 0);
|
|
92
|
-
_MedplumClient_clientId.set(this, void 0);
|
|
93
|
-
_MedplumClient_clientSecret.set(this, void 0);
|
|
94
|
-
_MedplumClient_autoBatchTimerId.set(this, void 0);
|
|
95
|
-
_MedplumClient_accessToken.set(this, void 0);
|
|
96
|
-
_MedplumClient_refreshToken.set(this, void 0);
|
|
97
|
-
_MedplumClient_refreshPromise.set(this, void 0);
|
|
98
|
-
_MedplumClient_profilePromise.set(this, void 0);
|
|
99
|
-
_MedplumClient_profile.set(this, void 0);
|
|
100
|
-
_MedplumClient_config.set(this, void 0);
|
|
101
76
|
if (options?.baseUrl) {
|
|
102
77
|
if (!options.baseUrl.startsWith('http')) {
|
|
103
78
|
throw new Error('Base URL must start with http or https');
|
|
104
79
|
}
|
|
105
80
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
}
|
|
120
107
|
const activeLogin = this.getActiveLogin();
|
|
121
108
|
if (activeLogin) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
109
|
+
this.accessToken = activeLogin.accessToken;
|
|
110
|
+
this.refreshToken = activeLogin.refreshToken;
|
|
111
|
+
this.refreshProfile().catch(console.log);
|
|
125
112
|
}
|
|
126
|
-
|
|
113
|
+
this.setupStorageListener();
|
|
127
114
|
}
|
|
128
115
|
/**
|
|
129
116
|
* Returns the current base URL for all API requests.
|
|
@@ -133,14 +120,14 @@ class MedplumClient extends EventTarget {
|
|
|
133
120
|
* @returns The current base URL for all API requests.
|
|
134
121
|
*/
|
|
135
122
|
getBaseUrl() {
|
|
136
|
-
return
|
|
123
|
+
return this.baseUrl;
|
|
137
124
|
}
|
|
138
125
|
/**
|
|
139
126
|
* Clears all auth state including local storage and session storage.
|
|
140
127
|
* @category Authentication
|
|
141
128
|
*/
|
|
142
129
|
clear() {
|
|
143
|
-
|
|
130
|
+
this.storage.clear();
|
|
144
131
|
this.clearActiveLogin();
|
|
145
132
|
}
|
|
146
133
|
/**
|
|
@@ -149,12 +136,12 @@ class MedplumClient extends EventTarget {
|
|
|
149
136
|
* @category Authentication
|
|
150
137
|
*/
|
|
151
138
|
clearActiveLogin() {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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;
|
|
158
145
|
this.dispatchEvent({ type: 'change' });
|
|
159
146
|
}
|
|
160
147
|
/**
|
|
@@ -164,7 +151,7 @@ class MedplumClient extends EventTarget {
|
|
|
164
151
|
*/
|
|
165
152
|
invalidateUrl(url) {
|
|
166
153
|
url = url.toString();
|
|
167
|
-
|
|
154
|
+
this.requestCache?.delete(url);
|
|
168
155
|
}
|
|
169
156
|
/**
|
|
170
157
|
* Invalidates all cached search results or cached requests for the given resourceType.
|
|
@@ -173,9 +160,11 @@ class MedplumClient extends EventTarget {
|
|
|
173
160
|
*/
|
|
174
161
|
invalidateSearches(resourceType) {
|
|
175
162
|
const url = 'fhir/R4/' + resourceType;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
168
|
}
|
|
180
169
|
}
|
|
181
170
|
}
|
|
@@ -193,30 +182,30 @@ class MedplumClient extends EventTarget {
|
|
|
193
182
|
*/
|
|
194
183
|
get(url, options = {}) {
|
|
195
184
|
url = url.toString();
|
|
196
|
-
const cached =
|
|
185
|
+
const cached = this.getCacheEntry(url, options);
|
|
197
186
|
if (cached) {
|
|
198
187
|
return cached.value;
|
|
199
188
|
}
|
|
200
189
|
let promise;
|
|
201
|
-
if (url.startsWith(
|
|
190
|
+
if (url.startsWith(this.fhirBaseUrl) && this.autoBatchQueue) {
|
|
202
191
|
promise = new Promise((resolve, reject) => {
|
|
203
|
-
|
|
192
|
+
this.autoBatchQueue.push({
|
|
204
193
|
method: 'GET',
|
|
205
|
-
url: url.replace(
|
|
194
|
+
url: url.replace(this.fhirBaseUrl, ''),
|
|
206
195
|
options,
|
|
207
196
|
resolve,
|
|
208
197
|
reject,
|
|
209
198
|
});
|
|
210
|
-
if (!
|
|
211
|
-
|
|
199
|
+
if (!this.autoBatchTimerId) {
|
|
200
|
+
this.autoBatchTimerId = setTimeout(() => this.executeAutoBatch(), this.autoBatchTime);
|
|
212
201
|
}
|
|
213
202
|
});
|
|
214
203
|
}
|
|
215
204
|
else {
|
|
216
|
-
promise =
|
|
205
|
+
promise = this.request('GET', url, options);
|
|
217
206
|
}
|
|
218
207
|
const readablePromise = new ReadablePromise(promise);
|
|
219
|
-
|
|
208
|
+
this.setCacheEntry(url, readablePromise);
|
|
220
209
|
return readablePromise;
|
|
221
210
|
}
|
|
222
211
|
/**
|
|
@@ -236,13 +225,13 @@ class MedplumClient extends EventTarget {
|
|
|
236
225
|
post(url, body, contentType, options = {}) {
|
|
237
226
|
url = url.toString();
|
|
238
227
|
if (body) {
|
|
239
|
-
|
|
228
|
+
this.setRequestBody(options, body);
|
|
240
229
|
}
|
|
241
230
|
if (contentType) {
|
|
242
|
-
|
|
231
|
+
this.setRequestContentType(options, contentType);
|
|
243
232
|
}
|
|
244
233
|
this.invalidateUrl(url);
|
|
245
|
-
return
|
|
234
|
+
return this.request('POST', url, options);
|
|
246
235
|
}
|
|
247
236
|
/**
|
|
248
237
|
* Makes an HTTP PUT request to the specified URL.
|
|
@@ -261,13 +250,13 @@ class MedplumClient extends EventTarget {
|
|
|
261
250
|
put(url, body, contentType, options = {}) {
|
|
262
251
|
url = url.toString();
|
|
263
252
|
if (body) {
|
|
264
|
-
|
|
253
|
+
this.setRequestBody(options, body);
|
|
265
254
|
}
|
|
266
255
|
if (contentType) {
|
|
267
|
-
|
|
256
|
+
this.setRequestContentType(options, contentType);
|
|
268
257
|
}
|
|
269
258
|
this.invalidateUrl(url);
|
|
270
|
-
return
|
|
259
|
+
return this.request('PUT', url, options);
|
|
271
260
|
}
|
|
272
261
|
/**
|
|
273
262
|
* Makes an HTTP PATCH request to the specified URL.
|
|
@@ -284,10 +273,10 @@ class MedplumClient extends EventTarget {
|
|
|
284
273
|
*/
|
|
285
274
|
patch(url, operations, options = {}) {
|
|
286
275
|
url = url.toString();
|
|
287
|
-
|
|
288
|
-
|
|
276
|
+
this.setRequestBody(options, operations);
|
|
277
|
+
this.setRequestContentType(options, PATCH_CONTENT_TYPE);
|
|
289
278
|
this.invalidateUrl(url);
|
|
290
|
-
return
|
|
279
|
+
return this.request('PATCH', url, options);
|
|
291
280
|
}
|
|
292
281
|
/**
|
|
293
282
|
* Makes an HTTP DELETE request to the specified URL.
|
|
@@ -305,7 +294,7 @@ class MedplumClient extends EventTarget {
|
|
|
305
294
|
delete(url, options = {}) {
|
|
306
295
|
url = url.toString();
|
|
307
296
|
this.invalidateUrl(url);
|
|
308
|
-
return
|
|
297
|
+
return this.request('DELETE', url, options);
|
|
309
298
|
}
|
|
310
299
|
/**
|
|
311
300
|
* Initiates a new user flow.
|
|
@@ -357,7 +346,7 @@ class MedplumClient extends EventTarget {
|
|
|
357
346
|
async startLogin(loginRequest) {
|
|
358
347
|
return this.post('auth/login', {
|
|
359
348
|
...(await this.ensureCodeChallenge(loginRequest)),
|
|
360
|
-
clientId: loginRequest.clientId ??
|
|
349
|
+
clientId: loginRequest.clientId ?? this.clientId,
|
|
361
350
|
scope: loginRequest.scope,
|
|
362
351
|
});
|
|
363
352
|
}
|
|
@@ -372,7 +361,7 @@ class MedplumClient extends EventTarget {
|
|
|
372
361
|
async startGoogleLogin(loginRequest) {
|
|
373
362
|
return this.post('auth/google', {
|
|
374
363
|
...(await this.ensureCodeChallenge(loginRequest)),
|
|
375
|
-
clientId: loginRequest.clientId ??
|
|
364
|
+
clientId: loginRequest.clientId ?? this.clientId,
|
|
376
365
|
scope: loginRequest.scope,
|
|
377
366
|
});
|
|
378
367
|
}
|
|
@@ -396,7 +385,7 @@ class MedplumClient extends EventTarget {
|
|
|
396
385
|
* @category Authentication
|
|
397
386
|
*/
|
|
398
387
|
async signOut() {
|
|
399
|
-
await this.post(
|
|
388
|
+
await this.post(this.logoutUrl, {});
|
|
400
389
|
this.clear();
|
|
401
390
|
}
|
|
402
391
|
/**
|
|
@@ -410,7 +399,7 @@ class MedplumClient extends EventTarget {
|
|
|
410
399
|
const urlParams = new URLSearchParams(window.location.search);
|
|
411
400
|
const code = urlParams.get('code');
|
|
412
401
|
if (!code) {
|
|
413
|
-
await
|
|
402
|
+
await this.requestAuthorization(loginParams);
|
|
414
403
|
return undefined;
|
|
415
404
|
}
|
|
416
405
|
else {
|
|
@@ -423,7 +412,7 @@ class MedplumClient extends EventTarget {
|
|
|
423
412
|
* @category Authentication
|
|
424
413
|
*/
|
|
425
414
|
signOutWithRedirect() {
|
|
426
|
-
window.location.assign(
|
|
415
|
+
window.location.assign(this.logoutUrl);
|
|
427
416
|
}
|
|
428
417
|
/**
|
|
429
418
|
* Initiates sign in with an external identity provider.
|
|
@@ -437,6 +426,34 @@ class MedplumClient extends EventTarget {
|
|
|
437
426
|
const loginRequest = await this.ensureCodeChallenge(baseLogin);
|
|
438
427
|
window.location.assign(this.getExternalAuthRedirectUri(authorizeUrl, clientId, redirectUri, loginRequest));
|
|
439
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Exchange an external access token for a Medplum access token.
|
|
431
|
+
* @param token The access token that was generated by the external identity provider.
|
|
432
|
+
* @param clientId The ID of the `ClientApplication` in your Medplum project that will be making the exchange request.
|
|
433
|
+
* @category Authentication
|
|
434
|
+
*/
|
|
435
|
+
async exchangeExternalAccessToken(token, clientId) {
|
|
436
|
+
clientId = clientId || this.clientId;
|
|
437
|
+
if (!clientId) {
|
|
438
|
+
throw new Error('MedplumClient is missing clientId');
|
|
439
|
+
}
|
|
440
|
+
const response = await this.fetch(this.exchangeUrl, {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
headers: { 'Content-Type': 'application/json' },
|
|
443
|
+
body: JSON.stringify({
|
|
444
|
+
clientId: this.clientId,
|
|
445
|
+
externalAccessToken: token,
|
|
446
|
+
}),
|
|
447
|
+
credentials: 'include',
|
|
448
|
+
});
|
|
449
|
+
if (!response.ok) {
|
|
450
|
+
this.clearActiveLogin();
|
|
451
|
+
throw new Error('Failed to fetch tokens');
|
|
452
|
+
}
|
|
453
|
+
const tokens = await response.json();
|
|
454
|
+
await this.verifyTokens(tokens);
|
|
455
|
+
return this.getProfile();
|
|
456
|
+
}
|
|
440
457
|
/**
|
|
441
458
|
* Builds the external identity provider redirect URI.
|
|
442
459
|
* @param authorizeUrl The external authorization URL.
|
|
@@ -463,7 +480,7 @@ class MedplumClient extends EventTarget {
|
|
|
463
480
|
* @returns The well-formed FHIR URL.
|
|
464
481
|
*/
|
|
465
482
|
fhirUrl(...path) {
|
|
466
|
-
return new URL(
|
|
483
|
+
return new URL(this.fhirBaseUrl + path.join('/'));
|
|
467
484
|
}
|
|
468
485
|
/**
|
|
469
486
|
* Builds a FHIR search URL from a search query or structured query object.
|
|
@@ -531,7 +548,7 @@ class MedplumClient extends EventTarget {
|
|
|
531
548
|
search(resourceType, query, options = {}) {
|
|
532
549
|
const url = this.fhirSearchUrl(resourceType, query);
|
|
533
550
|
const cacheKey = url.toString() + '-search';
|
|
534
|
-
const cached =
|
|
551
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
535
552
|
if (cached) {
|
|
536
553
|
return cached.value;
|
|
537
554
|
}
|
|
@@ -539,12 +556,12 @@ class MedplumClient extends EventTarget {
|
|
|
539
556
|
const bundle = await this.get(url, options);
|
|
540
557
|
if (bundle.entry) {
|
|
541
558
|
for (const entry of bundle.entry) {
|
|
542
|
-
|
|
559
|
+
this.cacheResource(entry.resource);
|
|
543
560
|
}
|
|
544
561
|
}
|
|
545
562
|
return bundle;
|
|
546
563
|
})());
|
|
547
|
-
|
|
564
|
+
this.setCacheEntry(cacheKey, promise);
|
|
548
565
|
return promise;
|
|
549
566
|
}
|
|
550
567
|
/**
|
|
@@ -574,12 +591,12 @@ class MedplumClient extends EventTarget {
|
|
|
574
591
|
url.searchParams.set('_count', '1');
|
|
575
592
|
url.searchParams.sort();
|
|
576
593
|
const cacheKey = url.toString() + '-searchOne';
|
|
577
|
-
const cached =
|
|
594
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
578
595
|
if (cached) {
|
|
579
596
|
return cached.value;
|
|
580
597
|
}
|
|
581
598
|
const promise = new ReadablePromise(this.search(resourceType, url.searchParams, options).then((b) => b.entry?.[0]?.resource));
|
|
582
|
-
|
|
599
|
+
this.setCacheEntry(cacheKey, promise);
|
|
583
600
|
return promise;
|
|
584
601
|
}
|
|
585
602
|
/**
|
|
@@ -607,14 +624,48 @@ class MedplumClient extends EventTarget {
|
|
|
607
624
|
searchResources(resourceType, query, options = {}) {
|
|
608
625
|
const url = this.fhirSearchUrl(resourceType, query);
|
|
609
626
|
const cacheKey = url.toString() + '-searchResources';
|
|
610
|
-
const cached =
|
|
627
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
611
628
|
if (cached) {
|
|
612
629
|
return cached.value;
|
|
613
630
|
}
|
|
614
631
|
const promise = new ReadablePromise(this.search(resourceType, query, options).then((b) => b.entry?.map((e) => e.resource) ?? []));
|
|
615
|
-
|
|
632
|
+
this.setCacheEntry(cacheKey, promise);
|
|
616
633
|
return promise;
|
|
617
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Creates an
|
|
637
|
+
* [async generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator)
|
|
638
|
+
* over a series of FHIR search requests for paginated search results. Each iteration of the generator yields
|
|
639
|
+
* the array of resources on each page.
|
|
640
|
+
*
|
|
641
|
+
*
|
|
642
|
+
* ```typescript
|
|
643
|
+
* for await (const page of medplum.searchResourcePages('Patient', { _count: 10 })) {
|
|
644
|
+
* for (const patient of page) {
|
|
645
|
+
* console.log(`Processing Patient resource with ID: ${patient.id}`);
|
|
646
|
+
* }
|
|
647
|
+
* }
|
|
648
|
+
* ```
|
|
649
|
+
*
|
|
650
|
+
* @category Search
|
|
651
|
+
* @param resourceType The FHIR resource type.
|
|
652
|
+
* @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
|
|
653
|
+
* @param options Optional fetch options.
|
|
654
|
+
* @returns An async generator, where each result is an array of resources for each page.
|
|
655
|
+
*/
|
|
656
|
+
async *searchResourcePages(resourceType, query, options = {}) {
|
|
657
|
+
let url = this.fhirSearchUrl(resourceType, query);
|
|
658
|
+
while (url) {
|
|
659
|
+
const searchParams = new URL(url).searchParams;
|
|
660
|
+
const bundle = await this.search(resourceType, searchParams, options);
|
|
661
|
+
const nextLink = bundle?.link?.find((link) => link.relation === 'next');
|
|
662
|
+
if (!bundle?.entry?.length && !nextLink) {
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
yield bundle?.entry?.map((e) => e.resource) ?? [];
|
|
666
|
+
url = nextLink?.url ? new URL(nextLink?.url) : undefined;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
618
669
|
/**
|
|
619
670
|
* Searches a ValueSet resource using the "expand" operation.
|
|
620
671
|
* See: https://www.hl7.org/fhir/operation-valueset-expand.html
|
|
@@ -639,7 +690,7 @@ class MedplumClient extends EventTarget {
|
|
|
639
690
|
* @returns The resource if it is available in the cache; undefined otherwise.
|
|
640
691
|
*/
|
|
641
692
|
getCached(resourceType, id) {
|
|
642
|
-
const cached =
|
|
693
|
+
const cached = this.requestCache?.get(this.fhirUrl(resourceType, id).toString())?.value;
|
|
643
694
|
return cached && cached.isOk() ? cached.read() : undefined;
|
|
644
695
|
}
|
|
645
696
|
/**
|
|
@@ -741,7 +792,7 @@ class MedplumClient extends EventTarget {
|
|
|
741
792
|
return Promise.resolve(globalSchema);
|
|
742
793
|
}
|
|
743
794
|
const cacheKey = resourceType + '-requestSchema';
|
|
744
|
-
const cached =
|
|
795
|
+
const cached = this.getCacheEntry(cacheKey, undefined);
|
|
745
796
|
if (cached) {
|
|
746
797
|
return cached.value;
|
|
747
798
|
}
|
|
@@ -784,7 +835,7 @@ class MedplumClient extends EventTarget {
|
|
|
784
835
|
}
|
|
785
836
|
return globalSchema;
|
|
786
837
|
})());
|
|
787
|
-
|
|
838
|
+
this.setCacheEntry(cacheKey, promise);
|
|
788
839
|
return promise;
|
|
789
840
|
}
|
|
790
841
|
/**
|
|
@@ -982,7 +1033,7 @@ class MedplumClient extends EventTarget {
|
|
|
982
1033
|
};
|
|
983
1034
|
xhr.open('POST', url);
|
|
984
1035
|
xhr.withCredentials = true;
|
|
985
|
-
xhr.setRequestHeader('Authorization', 'Bearer ' +
|
|
1036
|
+
xhr.setRequestHeader('Authorization', 'Bearer ' + this.accessToken);
|
|
986
1037
|
xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, max-age=0');
|
|
987
1038
|
xhr.setRequestHeader('Content-Type', contentType);
|
|
988
1039
|
xhr.setRequestHeader('X-Medplum', 'extended');
|
|
@@ -1012,10 +1063,10 @@ class MedplumClient extends EventTarget {
|
|
|
1012
1063
|
* @returns The result of the create operation.
|
|
1013
1064
|
*/
|
|
1014
1065
|
async createPdf(docDefinition, filename, tableLayouts, fonts) {
|
|
1015
|
-
if (!
|
|
1066
|
+
if (!this.createPdfImpl) {
|
|
1016
1067
|
throw new Error('PDF creation not enabled');
|
|
1017
1068
|
}
|
|
1018
|
-
const blob = await
|
|
1069
|
+
const blob = await this.createPdfImpl(docDefinition, tableLayouts, fonts);
|
|
1019
1070
|
return this.createBinary(blob, filename, 'application/pdf');
|
|
1020
1071
|
}
|
|
1021
1072
|
/**
|
|
@@ -1093,7 +1144,7 @@ class MedplumClient extends EventTarget {
|
|
|
1093
1144
|
// return result ?? resource;
|
|
1094
1145
|
result = resource;
|
|
1095
1146
|
}
|
|
1096
|
-
|
|
1147
|
+
this.cacheResource(result);
|
|
1097
1148
|
return result;
|
|
1098
1149
|
}
|
|
1099
1150
|
/**
|
|
@@ -1141,7 +1192,7 @@ class MedplumClient extends EventTarget {
|
|
|
1141
1192
|
* @returns The result of the delete operation.
|
|
1142
1193
|
*/
|
|
1143
1194
|
deleteResource(resourceType, id) {
|
|
1144
|
-
|
|
1195
|
+
this.deleteCacheEntry(this.fhirUrl(resourceType, id).toString());
|
|
1145
1196
|
this.invalidateSearches(resourceType);
|
|
1146
1197
|
return this.delete(this.fhirUrl(resourceType, id));
|
|
1147
1198
|
}
|
|
@@ -1345,61 +1396,80 @@ class MedplumClient extends EventTarget {
|
|
|
1345
1396
|
* @returns The Login State
|
|
1346
1397
|
*/
|
|
1347
1398
|
getActiveLogin() {
|
|
1348
|
-
return
|
|
1399
|
+
return this.storage.getObject('activeLogin');
|
|
1349
1400
|
}
|
|
1350
1401
|
/**
|
|
1351
1402
|
* @category Authentication
|
|
1352
1403
|
*/
|
|
1353
1404
|
async setActiveLogin(login) {
|
|
1354
1405
|
this.clearActiveLogin();
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
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();
|
|
1361
1412
|
}
|
|
1362
1413
|
/**
|
|
1363
1414
|
* Returns the current access token.
|
|
1364
1415
|
* @category Authentication
|
|
1365
1416
|
*/
|
|
1366
1417
|
getAccessToken() {
|
|
1367
|
-
return
|
|
1418
|
+
return this.accessToken;
|
|
1368
1419
|
}
|
|
1369
1420
|
/**
|
|
1370
1421
|
* Sets the current access token.
|
|
1371
1422
|
* @category Authentication
|
|
1372
1423
|
*/
|
|
1373
1424
|
setAccessToken(accessToken) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1425
|
+
this.accessToken = accessToken;
|
|
1426
|
+
this.refreshToken = undefined;
|
|
1427
|
+
this.profile = undefined;
|
|
1428
|
+
this.config = undefined;
|
|
1378
1429
|
}
|
|
1379
1430
|
/**
|
|
1380
1431
|
* @category Authentication
|
|
1381
1432
|
*/
|
|
1382
1433
|
getLogins() {
|
|
1383
|
-
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;
|
|
1384
1454
|
}
|
|
1385
1455
|
/**
|
|
1386
1456
|
* @category Authentication
|
|
1387
1457
|
*/
|
|
1388
1458
|
isLoading() {
|
|
1389
|
-
return !!
|
|
1459
|
+
return !!this.profilePromise;
|
|
1390
1460
|
}
|
|
1391
1461
|
/**
|
|
1392
1462
|
* @category User Profile
|
|
1393
1463
|
*/
|
|
1394
1464
|
getProfile() {
|
|
1395
|
-
return
|
|
1465
|
+
return this.profile;
|
|
1396
1466
|
}
|
|
1397
1467
|
/**
|
|
1398
1468
|
* @category User Profile
|
|
1399
1469
|
*/
|
|
1400
1470
|
async getProfileAsync() {
|
|
1401
|
-
if (
|
|
1402
|
-
await
|
|
1471
|
+
if (this.profilePromise) {
|
|
1472
|
+
await this.profilePromise;
|
|
1403
1473
|
}
|
|
1404
1474
|
return this.getProfile();
|
|
1405
1475
|
}
|
|
@@ -1407,7 +1477,7 @@ class MedplumClient extends EventTarget {
|
|
|
1407
1477
|
* @category User Profile
|
|
1408
1478
|
*/
|
|
1409
1479
|
getUserConfiguration() {
|
|
1410
|
-
return
|
|
1480
|
+
return this.config;
|
|
1411
1481
|
}
|
|
1412
1482
|
/**
|
|
1413
1483
|
* Downloads the URL as a blob.
|
|
@@ -1417,13 +1487,234 @@ class MedplumClient extends EventTarget {
|
|
|
1417
1487
|
* @returns Promise to the response body as a blob.
|
|
1418
1488
|
*/
|
|
1419
1489
|
async download(url, options = {}) {
|
|
1420
|
-
if (
|
|
1421
|
-
await
|
|
1490
|
+
if (this.refreshPromise) {
|
|
1491
|
+
await this.refreshPromise;
|
|
1422
1492
|
}
|
|
1423
|
-
|
|
1424
|
-
const response = await
|
|
1493
|
+
this.addFetchOptionsDefaults(options);
|
|
1494
|
+
const response = await this.fetch(url.toString(), options);
|
|
1425
1495
|
return response.blob();
|
|
1426
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
|
+
}
|
|
1427
1718
|
/**
|
|
1428
1719
|
* Starts a new PKCE flow.
|
|
1429
1720
|
* These PKCE values are stateful, and must survive redirects and page refreshes.
|
|
@@ -1439,6 +1730,23 @@ class MedplumClient extends EventTarget {
|
|
|
1439
1730
|
sessionStorage.setItem('codeChallenge', codeChallenge);
|
|
1440
1731
|
return { codeChallengeMethod: 'S256', codeChallenge };
|
|
1441
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
|
+
}
|
|
1442
1750
|
/**
|
|
1443
1751
|
* Processes an OAuth authorization code.
|
|
1444
1752
|
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
|
@@ -1450,7 +1758,7 @@ class MedplumClient extends EventTarget {
|
|
|
1450
1758
|
const formBody = new URLSearchParams();
|
|
1451
1759
|
formBody.set('grant_type', 'authorization_code');
|
|
1452
1760
|
formBody.set('code', code);
|
|
1453
|
-
formBody.set('client_id', loginParams?.clientId ||
|
|
1761
|
+
formBody.set('client_id', loginParams?.clientId || this.clientId);
|
|
1454
1762
|
formBody.set('redirect_uri', loginParams?.redirectUri || getWindowOrigin());
|
|
1455
1763
|
if (typeof sessionStorage !== 'undefined') {
|
|
1456
1764
|
const codeVerifier = sessionStorage.getItem('codeVerifier');
|
|
@@ -1458,7 +1766,29 @@ class MedplumClient extends EventTarget {
|
|
|
1458
1766
|
formBody.set('code_verifier', codeVerifier);
|
|
1459
1767
|
}
|
|
1460
1768
|
}
|
|
1461
|
-
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;
|
|
1462
1792
|
}
|
|
1463
1793
|
/**
|
|
1464
1794
|
* Starts a new OAuth2 client credentials flow.
|
|
@@ -1469,13 +1799,24 @@ class MedplumClient extends EventTarget {
|
|
|
1469
1799
|
* @returns Promise that resolves to the client profile.
|
|
1470
1800
|
*/
|
|
1471
1801
|
async startClientLogin(clientId, clientSecret) {
|
|
1472
|
-
|
|
1473
|
-
|
|
1802
|
+
this.clientId = clientId;
|
|
1803
|
+
this.clientSecret = clientSecret;
|
|
1474
1804
|
const formBody = new URLSearchParams();
|
|
1475
1805
|
formBody.set('grant_type', 'client_credentials');
|
|
1476
1806
|
formBody.set('client_id', clientId);
|
|
1477
1807
|
formBody.set('client_secret', clientSecret);
|
|
1478
|
-
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);
|
|
1479
1820
|
}
|
|
1480
1821
|
/**
|
|
1481
1822
|
* Invite a user to a project.
|
|
@@ -1486,275 +1827,72 @@ class MedplumClient extends EventTarget {
|
|
|
1486
1827
|
async invite(projectId, body) {
|
|
1487
1828
|
return this.post('admin/projects/' + projectId + '/invite', body);
|
|
1488
1829
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
.catch(reject);
|
|
1505
|
-
}), "f");
|
|
1506
|
-
return __classPrivateFieldGet(this, _MedplumClient_profilePromise, "f");
|
|
1507
|
-
}, _MedplumClient_getCacheEntry = function _MedplumClient_getCacheEntry(key, options) {
|
|
1508
|
-
if (__classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") <= 0 || options?.cache === 'no-cache' || options?.cache === 'reload') {
|
|
1509
|
-
return undefined;
|
|
1510
|
-
}
|
|
1511
|
-
const entry = __classPrivateFieldGet(this, _MedplumClient_requestCache, "f").get(key);
|
|
1512
|
-
if (!entry || entry.requestTime + __classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") < Date.now()) {
|
|
1513
|
-
return undefined;
|
|
1514
|
-
}
|
|
1515
|
-
return entry;
|
|
1516
|
-
}, _MedplumClient_setCacheEntry = function _MedplumClient_setCacheEntry(key, value) {
|
|
1517
|
-
if (__classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") > 0) {
|
|
1518
|
-
__classPrivateFieldGet(this, _MedplumClient_requestCache, "f").set(key, { requestTime: Date.now(), value });
|
|
1519
|
-
}
|
|
1520
|
-
}, _MedplumClient_cacheResource = function _MedplumClient_cacheResource(resource) {
|
|
1521
|
-
if (resource?.id) {
|
|
1522
|
-
__classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_setCacheEntry).call(this, this.fhirUrl(resource.resourceType, resource.id).toString(), new ReadablePromise(Promise.resolve(resource)));
|
|
1523
|
-
}
|
|
1524
|
-
}, _MedplumClient_deleteCacheEntry = function _MedplumClient_deleteCacheEntry(key) {
|
|
1525
|
-
if (__classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") > 0) {
|
|
1526
|
-
__classPrivateFieldGet(this, _MedplumClient_requestCache, "f").delete(key);
|
|
1527
|
-
}
|
|
1528
|
-
}, _MedplumClient_request =
|
|
1529
|
-
/**
|
|
1530
|
-
* Makes an HTTP request.
|
|
1531
|
-
* @param {string} method
|
|
1532
|
-
* @param {string} url
|
|
1533
|
-
* @param {string=} contentType
|
|
1534
|
-
* @param {Object=} body
|
|
1535
|
-
*/
|
|
1536
|
-
async function _MedplumClient_request(method, url, options = {}) {
|
|
1537
|
-
if (__classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f")) {
|
|
1538
|
-
await __classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f");
|
|
1539
|
-
}
|
|
1540
|
-
if (!url.startsWith('http')) {
|
|
1541
|
-
url = __classPrivateFieldGet(this, _MedplumClient_baseUrl, "f") + url;
|
|
1542
|
-
}
|
|
1543
|
-
options.method = method;
|
|
1544
|
-
__classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_addFetchOptionsDefaults).call(this, options);
|
|
1545
|
-
const response = await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_fetchWithRetry).call(this, url, options);
|
|
1546
|
-
if (response.status === 401) {
|
|
1547
|
-
// Refresh and try again
|
|
1548
|
-
return __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_handleUnauthenticated).call(this, method, url, options);
|
|
1549
|
-
}
|
|
1550
|
-
if (response.status === 204 || response.status === 304) {
|
|
1551
|
-
// No content or change
|
|
1552
|
-
return undefined;
|
|
1553
|
-
}
|
|
1554
|
-
let obj = undefined;
|
|
1555
|
-
try {
|
|
1556
|
-
obj = await response.json();
|
|
1557
|
-
}
|
|
1558
|
-
catch (err) {
|
|
1559
|
-
console.error('Error parsing response', response.status, err);
|
|
1560
|
-
throw err;
|
|
1561
|
-
}
|
|
1562
|
-
if (response.status >= 400) {
|
|
1563
|
-
throw new OperationOutcomeError(normalizeOperationOutcome(obj));
|
|
1564
|
-
}
|
|
1565
|
-
return obj;
|
|
1566
|
-
}, _MedplumClient_fetchWithRetry = async function _MedplumClient_fetchWithRetry(url, options) {
|
|
1567
|
-
const maxRetries = 3;
|
|
1568
|
-
const retryDelay = 200;
|
|
1569
|
-
let response = undefined;
|
|
1570
|
-
for (let retry = 0; retry < maxRetries; retry++) {
|
|
1571
|
-
response = (await __classPrivateFieldGet(this, _MedplumClient_fetch, "f").call(this, url, options));
|
|
1572
|
-
if (response.status < 500) {
|
|
1573
|
-
return response;
|
|
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');
|
|
1574
1845
|
}
|
|
1575
|
-
|
|
1846
|
+
const tokens = await response.json();
|
|
1847
|
+
await this.verifyTokens(tokens);
|
|
1848
|
+
return this.getProfile();
|
|
1576
1849
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
if (entries.length === 1) {
|
|
1591
|
-
const entry = entries[0];
|
|
1592
|
-
entry.resolve(await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_request).call(this, entry.method, __classPrivateFieldGet(this, _MedplumClient_fhirBaseUrl, "f") + entry.url, entry.options));
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
// Build the batch request
|
|
1596
|
-
const batch = {
|
|
1597
|
-
resourceType: 'Bundle',
|
|
1598
|
-
type: 'batch',
|
|
1599
|
-
entry: entries.map((e) => ({
|
|
1600
|
-
request: {
|
|
1601
|
-
method: e.method,
|
|
1602
|
-
url: e.url,
|
|
1603
|
-
},
|
|
1604
|
-
resource: e.options.body ? JSON.parse(e.options.body) : undefined,
|
|
1605
|
-
})),
|
|
1606
|
-
};
|
|
1607
|
-
// Execute the batch request
|
|
1608
|
-
const response = (await this.post('fhir/R4', batch));
|
|
1609
|
-
// Process the response
|
|
1610
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1611
|
-
const entry = entries[i];
|
|
1612
|
-
const responseEntry = response.entry?.[i];
|
|
1613
|
-
if (responseEntry?.response?.outcome && !isOk(responseEntry.response.outcome)) {
|
|
1614
|
-
entry.reject(new OperationOutcomeError(responseEntry.response.outcome));
|
|
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');
|
|
1615
1863
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
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');
|
|
1618
1868
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
const headers = options.headers;
|
|
1625
|
-
headers['X-Medplum'] = 'extended';
|
|
1626
|
-
if (!headers['Content-Type']) {
|
|
1627
|
-
headers['Content-Type'] = FHIR_CONTENT_TYPE;
|
|
1628
|
-
}
|
|
1629
|
-
if (__classPrivateFieldGet(this, _MedplumClient_accessToken, "f")) {
|
|
1630
|
-
headers['Authorization'] = 'Bearer ' + __classPrivateFieldGet(this, _MedplumClient_accessToken, "f");
|
|
1631
|
-
}
|
|
1632
|
-
if (!options.cache) {
|
|
1633
|
-
options.cache = 'no-cache';
|
|
1634
|
-
}
|
|
1635
|
-
if (!options.credentials) {
|
|
1636
|
-
options.credentials = 'include';
|
|
1637
|
-
}
|
|
1638
|
-
}, _MedplumClient_setRequestContentType = function _MedplumClient_setRequestContentType(options, contentType) {
|
|
1639
|
-
if (!options.headers) {
|
|
1640
|
-
options.headers = {};
|
|
1641
|
-
}
|
|
1642
|
-
const headers = options.headers;
|
|
1643
|
-
headers['Content-Type'] = contentType;
|
|
1644
|
-
}, _MedplumClient_setRequestBody = function _MedplumClient_setRequestBody(options, data) {
|
|
1645
|
-
if (typeof data === 'string' ||
|
|
1646
|
-
(typeof Blob !== 'undefined' && data instanceof Blob) ||
|
|
1647
|
-
(typeof File !== 'undefined' && data instanceof File) ||
|
|
1648
|
-
(typeof Uint8Array !== 'undefined' && data instanceof Uint8Array)) {
|
|
1649
|
-
options.body = data;
|
|
1650
|
-
}
|
|
1651
|
-
else if (data) {
|
|
1652
|
-
options.body = JSON.stringify(data);
|
|
1653
|
-
}
|
|
1654
|
-
}, _MedplumClient_handleUnauthenticated = function _MedplumClient_handleUnauthenticated(method, url, options) {
|
|
1655
|
-
if (__classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_refresh).call(this)) {
|
|
1656
|
-
return __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_request).call(this, method, url, options);
|
|
1657
|
-
}
|
|
1658
|
-
this.clearActiveLogin();
|
|
1659
|
-
if (__classPrivateFieldGet(this, _MedplumClient_onUnauthenticated, "f")) {
|
|
1660
|
-
__classPrivateFieldGet(this, _MedplumClient_onUnauthenticated, "f").call(this);
|
|
1661
|
-
}
|
|
1662
|
-
return Promise.reject(new Error('Unauthenticated'));
|
|
1663
|
-
}, _MedplumClient_requestAuthorization =
|
|
1664
|
-
/**
|
|
1665
|
-
* Redirects the user to the login screen for authorization.
|
|
1666
|
-
* Clears all auth state including local storage and session storage.
|
|
1667
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
|
1668
|
-
*/
|
|
1669
|
-
async function _MedplumClient_requestAuthorization(loginParams) {
|
|
1670
|
-
const loginRequest = await this.ensureCodeChallenge(loginParams || {});
|
|
1671
|
-
const url = new URL(__classPrivateFieldGet(this, _MedplumClient_authorizeUrl, "f"));
|
|
1672
|
-
url.searchParams.set('response_type', 'code');
|
|
1673
|
-
url.searchParams.set('state', sessionStorage.getItem('pkceState'));
|
|
1674
|
-
url.searchParams.set('client_id', loginRequest.clientId || __classPrivateFieldGet(this, _MedplumClient_clientId, "f"));
|
|
1675
|
-
url.searchParams.set('redirect_uri', loginRequest.redirectUri || getWindowOrigin());
|
|
1676
|
-
url.searchParams.set('code_challenge_method', loginRequest.codeChallengeMethod);
|
|
1677
|
-
url.searchParams.set('code_challenge', loginRequest.codeChallenge);
|
|
1678
|
-
url.searchParams.set('scope', loginRequest.scope || 'openid profile');
|
|
1679
|
-
window.location.assign(url.toString());
|
|
1680
|
-
}, _MedplumClient_refresh = function _MedplumClient_refresh() {
|
|
1681
|
-
if (__classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f")) {
|
|
1682
|
-
return __classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f");
|
|
1683
|
-
}
|
|
1684
|
-
if (__classPrivateFieldGet(this, _MedplumClient_refreshToken, "f")) {
|
|
1685
|
-
const formBody = new URLSearchParams();
|
|
1686
|
-
formBody.set('grant_type', 'refresh_token');
|
|
1687
|
-
formBody.set('client_id', __classPrivateFieldGet(this, _MedplumClient_clientId, "f"));
|
|
1688
|
-
formBody.set('refresh_token', __classPrivateFieldGet(this, _MedplumClient_refreshToken, "f"));
|
|
1689
|
-
__classPrivateFieldSet(this, _MedplumClient_refreshPromise, __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_fetchTokens).call(this, formBody), "f");
|
|
1690
|
-
return __classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f");
|
|
1691
|
-
}
|
|
1692
|
-
if (__classPrivateFieldGet(this, _MedplumClient_clientId, "f") && __classPrivateFieldGet(this, _MedplumClient_clientSecret, "f")) {
|
|
1693
|
-
__classPrivateFieldSet(this, _MedplumClient_refreshPromise, this.startClientLogin(__classPrivateFieldGet(this, _MedplumClient_clientId, "f"), __classPrivateFieldGet(this, _MedplumClient_clientSecret, "f")), "f");
|
|
1694
|
-
return __classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f");
|
|
1695
|
-
}
|
|
1696
|
-
return undefined;
|
|
1697
|
-
}, _MedplumClient_fetchTokens =
|
|
1698
|
-
/**
|
|
1699
|
-
* Makes a POST request to the tokens endpoint.
|
|
1700
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
1701
|
-
* @param formBody Token parameters in URL encoded format.
|
|
1702
|
-
*/
|
|
1703
|
-
async function _MedplumClient_fetchTokens(formBody) {
|
|
1704
|
-
const response = await __classPrivateFieldGet(this, _MedplumClient_fetch, "f").call(this, __classPrivateFieldGet(this, _MedplumClient_tokenUrl, "f"), {
|
|
1705
|
-
method: 'POST',
|
|
1706
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1707
|
-
body: formBody,
|
|
1708
|
-
credentials: 'include',
|
|
1709
|
-
});
|
|
1710
|
-
if (!response.ok) {
|
|
1711
|
-
this.clearActiveLogin();
|
|
1712
|
-
throw new Error('Failed to fetch tokens');
|
|
1713
|
-
}
|
|
1714
|
-
const tokens = await response.json();
|
|
1715
|
-
await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_verifyTokens).call(this, tokens);
|
|
1716
|
-
return this.getProfile();
|
|
1717
|
-
}, _MedplumClient_verifyTokens =
|
|
1718
|
-
/**
|
|
1719
|
-
* Verifies the tokens received from the auth server.
|
|
1720
|
-
* Validates the JWT against the JWKS.
|
|
1721
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
1722
|
-
* @param tokens
|
|
1723
|
-
*/
|
|
1724
|
-
async function _MedplumClient_verifyTokens(tokens) {
|
|
1725
|
-
const token = tokens.access_token;
|
|
1726
|
-
// Verify token has not expired
|
|
1727
|
-
const tokenPayload = parseJWTPayload(token);
|
|
1728
|
-
if (Date.now() >= tokenPayload.exp * 1000) {
|
|
1729
|
-
this.clearActiveLogin();
|
|
1730
|
-
throw new Error('Token expired');
|
|
1731
|
-
}
|
|
1732
|
-
// Verify app_client_id
|
|
1733
|
-
if (__classPrivateFieldGet(this, _MedplumClient_clientId, "f") && tokenPayload.client_id !== __classPrivateFieldGet(this, _MedplumClient_clientId, "f")) {
|
|
1734
|
-
this.clearActiveLogin();
|
|
1735
|
-
throw new Error('Token was not issued for this audience');
|
|
1736
|
-
}
|
|
1737
|
-
return this.setActiveLogin({
|
|
1738
|
-
accessToken: token,
|
|
1739
|
-
refreshToken: tokens.refresh_token,
|
|
1740
|
-
project: tokens.project,
|
|
1741
|
-
profile: tokens.profile,
|
|
1742
|
-
});
|
|
1743
|
-
}, _MedplumClient_setupStorageListener = function _MedplumClient_setupStorageListener() {
|
|
1744
|
-
try {
|
|
1745
|
-
window.addEventListener('storage', (e) => {
|
|
1746
|
-
if (e.key === null || e.key === 'activeLogin') {
|
|
1747
|
-
// Storage events fire when different tabs make changes.
|
|
1748
|
-
// On storage clear (key === null) or activeLogin change (key === 'activeLogin')
|
|
1749
|
-
// Refresh the page to ensure the active login is up to date.
|
|
1750
|
-
window.location.reload();
|
|
1751
|
-
}
|
|
1869
|
+
return this.setActiveLogin({
|
|
1870
|
+
accessToken: token,
|
|
1871
|
+
refreshToken: tokens.refresh_token,
|
|
1872
|
+
project: tokens.project,
|
|
1873
|
+
profile: tokens.profile,
|
|
1752
1874
|
});
|
|
1753
1875
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1876
|
+
/**
|
|
1877
|
+
* Sets up a listener for window storage events.
|
|
1878
|
+
* This synchronizes state across browser windows and browser tabs.
|
|
1879
|
+
*/
|
|
1880
|
+
setupStorageListener() {
|
|
1881
|
+
try {
|
|
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
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
catch (err) {
|
|
1892
|
+
// Silently ignore if this environment does not support storage events
|
|
1893
|
+
}
|
|
1756
1894
|
}
|
|
1757
|
-
}
|
|
1895
|
+
}
|
|
1758
1896
|
/**
|
|
1759
1897
|
* Returns the default fetch method.
|
|
1760
1898
|
* The default fetch is currently only available in browser environments.
|