@medplum/core 2.0.14 → 2.0.16
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 +1244 -1101
- 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 +500 -413
- 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/format.mjs +24 -15
- package/dist/esm/format.mjs.map +1 -1
- package/dist/esm/index.min.mjs +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/jwt.mjs +2 -9
- package/dist/esm/jwt.mjs.map +1 -1
- package/dist/esm/outcomes.mjs +15 -1
- package/dist/esm/outcomes.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/search/search.mjs +15 -9
- package/dist/esm/search/search.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 +153 -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/format.d.ts +3 -1
- package/dist/types/outcomes.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_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.16-816ad9bf";
|
|
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
|
|
@@ -20,6 +18,35 @@ const JSON_CONTENT_TYPE = 'application/json';
|
|
|
20
18
|
const FHIR_CONTENT_TYPE = 'application/fhir+json';
|
|
21
19
|
const PATCH_CONTENT_TYPE = 'application/json-patch+json';
|
|
22
20
|
const system = { resourceType: 'Device', id: 'system', deviceName: [{ name: 'System' }] };
|
|
21
|
+
/**
|
|
22
|
+
* OAuth 2.0 Grant Type Identifiers
|
|
23
|
+
* Standard identifiers defined here: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#name-grant-types
|
|
24
|
+
* Token exchange extension defined here: https://datatracker.ietf.org/doc/html/rfc8693
|
|
25
|
+
*/
|
|
26
|
+
var OAuthGrantType;
|
|
27
|
+
(function (OAuthGrantType) {
|
|
28
|
+
OAuthGrantType["ClientCredentials"] = "client_credentials";
|
|
29
|
+
OAuthGrantType["AuthorizationCode"] = "authorization_code";
|
|
30
|
+
OAuthGrantType["RefreshToken"] = "refresh_token";
|
|
31
|
+
OAuthGrantType["TokenExchange"] = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
32
|
+
})(OAuthGrantType || (OAuthGrantType = {}));
|
|
33
|
+
/**
|
|
34
|
+
* OAuth 2.0 Token Type Identifiers
|
|
35
|
+
* See: https://datatracker.ietf.org/doc/html/rfc8693#name-token-type-identifiers
|
|
36
|
+
*/
|
|
37
|
+
var OAuthTokenType;
|
|
38
|
+
(function (OAuthTokenType) {
|
|
39
|
+
/** Indicates that the token is an OAuth 2.0 access token issued by the given authorization server. */
|
|
40
|
+
OAuthTokenType["AccessToken"] = "urn:ietf:params:oauth:token-type:access_token";
|
|
41
|
+
/** Indicates that the token is an OAuth 2.0 refresh token issued by the given authorization server. */
|
|
42
|
+
OAuthTokenType["RefreshToken"] = "urn:ietf:params:oauth:token-type:refresh_token";
|
|
43
|
+
/** Indicates that the token is an ID Token as defined in Section 2 of [OpenID.Core]. */
|
|
44
|
+
OAuthTokenType["IdToken"] = "urn:ietf:params:oauth:token-type:id_token";
|
|
45
|
+
/** Indicates that the token is a base64url-encoded SAML 1.1 [OASIS.saml-core-1.1] assertion. */
|
|
46
|
+
OAuthTokenType["Saml1Token"] = "urn:ietf:params:oauth:token-type:saml1";
|
|
47
|
+
/** Indicates that the token is a base64url-encoded SAML 2.0 [OASIS.saml-core-2.0-os] assertion. */
|
|
48
|
+
OAuthTokenType["Saml2Token"] = "urn:ietf:params:oauth:token-type:saml2";
|
|
49
|
+
})(OAuthTokenType || (OAuthTokenType = {}));
|
|
23
50
|
/**
|
|
24
51
|
* The MedplumClient class provides a client for the Medplum FHIR server.
|
|
25
52
|
*
|
|
@@ -75,57 +102,43 @@ const system = { resourceType: 'Device', id: 'system', deviceName: [{ name: 'Sys
|
|
|
75
102
|
class MedplumClient extends EventTarget {
|
|
76
103
|
constructor(options) {
|
|
77
104
|
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
105
|
if (options?.baseUrl) {
|
|
103
106
|
if (!options.baseUrl.startsWith('http')) {
|
|
104
107
|
throw new Error('Base URL must start with http or https');
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
this.fetch = options?.fetch || getDefaultFetch();
|
|
111
|
+
this.storage = options?.storage || new ClientStorage();
|
|
112
|
+
this.createPdfImpl = options?.createPdf;
|
|
113
|
+
this.baseUrl = ensureTrailingSlash(options?.baseUrl) || DEFAULT_BASE_URL;
|
|
114
|
+
this.fhirBaseUrl = this.baseUrl + (ensureTrailingSlash(options?.fhirUrlPath) || 'fhir/R4/');
|
|
115
|
+
this.clientId = options?.clientId || '';
|
|
116
|
+
this.authorizeUrl = options?.authorizeUrl || this.baseUrl + 'oauth2/authorize';
|
|
117
|
+
this.tokenUrl = options?.tokenUrl || this.baseUrl + 'oauth2/token';
|
|
118
|
+
this.logoutUrl = options?.logoutUrl || this.baseUrl + 'oauth2/logout';
|
|
119
|
+
this.onUnauthenticated = options?.onUnauthenticated;
|
|
120
|
+
this.cacheTime = options?.cacheTime ?? DEFAULT_CACHE_TIME;
|
|
121
|
+
if (this.cacheTime > 0) {
|
|
122
|
+
this.requestCache = new LRUCache(options?.resourceCacheSize ?? DEFAULT_RESOURCE_CACHE_SIZE);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.requestCache = undefined;
|
|
126
|
+
}
|
|
127
|
+
if (options?.autoBatchTime) {
|
|
128
|
+
this.autoBatchTime = options?.autoBatchTime ?? 0;
|
|
129
|
+
this.autoBatchQueue = [];
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.autoBatchTime = 0;
|
|
133
|
+
this.autoBatchQueue = undefined;
|
|
134
|
+
}
|
|
122
135
|
const activeLogin = this.getActiveLogin();
|
|
123
136
|
if (activeLogin) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
this.accessToken = activeLogin.accessToken;
|
|
138
|
+
this.refreshToken = activeLogin.refreshToken;
|
|
139
|
+
this.refreshProfile().catch(console.log);
|
|
127
140
|
}
|
|
128
|
-
|
|
141
|
+
this.setupStorageListener();
|
|
129
142
|
}
|
|
130
143
|
/**
|
|
131
144
|
* Returns the current base URL for all API requests.
|
|
@@ -135,14 +148,14 @@ class MedplumClient extends EventTarget {
|
|
|
135
148
|
* @returns The current base URL for all API requests.
|
|
136
149
|
*/
|
|
137
150
|
getBaseUrl() {
|
|
138
|
-
return
|
|
151
|
+
return this.baseUrl;
|
|
139
152
|
}
|
|
140
153
|
/**
|
|
141
154
|
* Clears all auth state including local storage and session storage.
|
|
142
155
|
* @category Authentication
|
|
143
156
|
*/
|
|
144
157
|
clear() {
|
|
145
|
-
|
|
158
|
+
this.storage.clear();
|
|
146
159
|
this.clearActiveLogin();
|
|
147
160
|
}
|
|
148
161
|
/**
|
|
@@ -151,12 +164,12 @@ class MedplumClient extends EventTarget {
|
|
|
151
164
|
* @category Authentication
|
|
152
165
|
*/
|
|
153
166
|
clearActiveLogin() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
this.storage.setString('activeLogin', undefined);
|
|
168
|
+
this.requestCache?.clear();
|
|
169
|
+
this.accessToken = undefined;
|
|
170
|
+
this.refreshToken = undefined;
|
|
171
|
+
this.profile = undefined;
|
|
172
|
+
this.config = undefined;
|
|
160
173
|
this.dispatchEvent({ type: 'change' });
|
|
161
174
|
}
|
|
162
175
|
/**
|
|
@@ -166,7 +179,7 @@ class MedplumClient extends EventTarget {
|
|
|
166
179
|
*/
|
|
167
180
|
invalidateUrl(url) {
|
|
168
181
|
url = url.toString();
|
|
169
|
-
|
|
182
|
+
this.requestCache?.delete(url);
|
|
170
183
|
}
|
|
171
184
|
/**
|
|
172
185
|
* Invalidates all cached search results or cached requests for the given resourceType.
|
|
@@ -174,10 +187,12 @@ class MedplumClient extends EventTarget {
|
|
|
174
187
|
* @param resourceType The resource type to invalidate.
|
|
175
188
|
*/
|
|
176
189
|
invalidateSearches(resourceType) {
|
|
177
|
-
const url =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
const url = this.fhirBaseUrl + resourceType;
|
|
191
|
+
if (this.requestCache) {
|
|
192
|
+
for (const key of this.requestCache.keys()) {
|
|
193
|
+
if (key.endsWith(url) || key.includes(url + '?')) {
|
|
194
|
+
this.requestCache.delete(key);
|
|
195
|
+
}
|
|
181
196
|
}
|
|
182
197
|
}
|
|
183
198
|
}
|
|
@@ -195,30 +210,30 @@ class MedplumClient extends EventTarget {
|
|
|
195
210
|
*/
|
|
196
211
|
get(url, options = {}) {
|
|
197
212
|
url = url.toString();
|
|
198
|
-
const cached =
|
|
213
|
+
const cached = this.getCacheEntry(url, options);
|
|
199
214
|
if (cached) {
|
|
200
215
|
return cached.value;
|
|
201
216
|
}
|
|
202
217
|
let promise;
|
|
203
|
-
if (url.startsWith(
|
|
218
|
+
if (url.startsWith(this.fhirBaseUrl) && this.autoBatchQueue) {
|
|
204
219
|
promise = new Promise((resolve, reject) => {
|
|
205
|
-
|
|
220
|
+
this.autoBatchQueue.push({
|
|
206
221
|
method: 'GET',
|
|
207
|
-
url: url.replace(
|
|
222
|
+
url: url.replace(this.fhirBaseUrl, ''),
|
|
208
223
|
options,
|
|
209
224
|
resolve,
|
|
210
225
|
reject,
|
|
211
226
|
});
|
|
212
|
-
if (!
|
|
213
|
-
|
|
227
|
+
if (!this.autoBatchTimerId) {
|
|
228
|
+
this.autoBatchTimerId = setTimeout(() => this.executeAutoBatch(), this.autoBatchTime);
|
|
214
229
|
}
|
|
215
230
|
});
|
|
216
231
|
}
|
|
217
232
|
else {
|
|
218
|
-
promise =
|
|
233
|
+
promise = this.request('GET', url, options);
|
|
219
234
|
}
|
|
220
235
|
const readablePromise = new ReadablePromise(promise);
|
|
221
|
-
|
|
236
|
+
this.setCacheEntry(url, readablePromise);
|
|
222
237
|
return readablePromise;
|
|
223
238
|
}
|
|
224
239
|
/**
|
|
@@ -238,13 +253,13 @@ class MedplumClient extends EventTarget {
|
|
|
238
253
|
post(url, body, contentType, options = {}) {
|
|
239
254
|
url = url.toString();
|
|
240
255
|
if (body) {
|
|
241
|
-
|
|
256
|
+
this.setRequestBody(options, body);
|
|
242
257
|
}
|
|
243
258
|
if (contentType) {
|
|
244
|
-
|
|
259
|
+
this.setRequestContentType(options, contentType);
|
|
245
260
|
}
|
|
246
261
|
this.invalidateUrl(url);
|
|
247
|
-
return
|
|
262
|
+
return this.request('POST', url, options);
|
|
248
263
|
}
|
|
249
264
|
/**
|
|
250
265
|
* Makes an HTTP PUT request to the specified URL.
|
|
@@ -263,13 +278,13 @@ class MedplumClient extends EventTarget {
|
|
|
263
278
|
put(url, body, contentType, options = {}) {
|
|
264
279
|
url = url.toString();
|
|
265
280
|
if (body) {
|
|
266
|
-
|
|
281
|
+
this.setRequestBody(options, body);
|
|
267
282
|
}
|
|
268
283
|
if (contentType) {
|
|
269
|
-
|
|
284
|
+
this.setRequestContentType(options, contentType);
|
|
270
285
|
}
|
|
271
286
|
this.invalidateUrl(url);
|
|
272
|
-
return
|
|
287
|
+
return this.request('PUT', url, options);
|
|
273
288
|
}
|
|
274
289
|
/**
|
|
275
290
|
* Makes an HTTP PATCH request to the specified URL.
|
|
@@ -286,10 +301,10 @@ class MedplumClient extends EventTarget {
|
|
|
286
301
|
*/
|
|
287
302
|
patch(url, operations, options = {}) {
|
|
288
303
|
url = url.toString();
|
|
289
|
-
|
|
290
|
-
|
|
304
|
+
this.setRequestBody(options, operations);
|
|
305
|
+
this.setRequestContentType(options, PATCH_CONTENT_TYPE);
|
|
291
306
|
this.invalidateUrl(url);
|
|
292
|
-
return
|
|
307
|
+
return this.request('PATCH', url, options);
|
|
293
308
|
}
|
|
294
309
|
/**
|
|
295
310
|
* Makes an HTTP DELETE request to the specified URL.
|
|
@@ -307,7 +322,7 @@ class MedplumClient extends EventTarget {
|
|
|
307
322
|
delete(url, options = {}) {
|
|
308
323
|
url = url.toString();
|
|
309
324
|
this.invalidateUrl(url);
|
|
310
|
-
return
|
|
325
|
+
return this.request('DELETE', url, options);
|
|
311
326
|
}
|
|
312
327
|
/**
|
|
313
328
|
* Initiates a new user flow.
|
|
@@ -359,7 +374,7 @@ class MedplumClient extends EventTarget {
|
|
|
359
374
|
async startLogin(loginRequest) {
|
|
360
375
|
return this.post('auth/login', {
|
|
361
376
|
...(await this.ensureCodeChallenge(loginRequest)),
|
|
362
|
-
clientId: loginRequest.clientId ??
|
|
377
|
+
clientId: loginRequest.clientId ?? this.clientId,
|
|
363
378
|
scope: loginRequest.scope,
|
|
364
379
|
});
|
|
365
380
|
}
|
|
@@ -374,7 +389,7 @@ class MedplumClient extends EventTarget {
|
|
|
374
389
|
async startGoogleLogin(loginRequest) {
|
|
375
390
|
return this.post('auth/google', {
|
|
376
391
|
...(await this.ensureCodeChallenge(loginRequest)),
|
|
377
|
-
clientId: loginRequest.clientId ??
|
|
392
|
+
clientId: loginRequest.clientId ?? this.clientId,
|
|
378
393
|
scope: loginRequest.scope,
|
|
379
394
|
});
|
|
380
395
|
}
|
|
@@ -398,7 +413,7 @@ class MedplumClient extends EventTarget {
|
|
|
398
413
|
* @category Authentication
|
|
399
414
|
*/
|
|
400
415
|
async signOut() {
|
|
401
|
-
await this.post(
|
|
416
|
+
await this.post(this.logoutUrl, {});
|
|
402
417
|
this.clear();
|
|
403
418
|
}
|
|
404
419
|
/**
|
|
@@ -412,7 +427,7 @@ class MedplumClient extends EventTarget {
|
|
|
412
427
|
const urlParams = new URLSearchParams(window.location.search);
|
|
413
428
|
const code = urlParams.get('code');
|
|
414
429
|
if (!code) {
|
|
415
|
-
await
|
|
430
|
+
await this.requestAuthorization(loginParams);
|
|
416
431
|
return undefined;
|
|
417
432
|
}
|
|
418
433
|
else {
|
|
@@ -425,7 +440,7 @@ class MedplumClient extends EventTarget {
|
|
|
425
440
|
* @category Authentication
|
|
426
441
|
*/
|
|
427
442
|
signOutWithRedirect() {
|
|
428
|
-
window.location.assign(
|
|
443
|
+
window.location.assign(this.logoutUrl);
|
|
429
444
|
}
|
|
430
445
|
/**
|
|
431
446
|
* Initiates sign in with an external identity provider.
|
|
@@ -446,26 +461,16 @@ class MedplumClient extends EventTarget {
|
|
|
446
461
|
* @category Authentication
|
|
447
462
|
*/
|
|
448
463
|
async exchangeExternalAccessToken(token, clientId) {
|
|
449
|
-
clientId = clientId ||
|
|
464
|
+
clientId = clientId || this.clientId;
|
|
450
465
|
if (!clientId) {
|
|
451
466
|
throw new Error('MedplumClient is missing clientId');
|
|
452
467
|
}
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}),
|
|
460
|
-
credentials: 'include',
|
|
461
|
-
});
|
|
462
|
-
if (!response.ok) {
|
|
463
|
-
this.clearActiveLogin();
|
|
464
|
-
throw new Error('Failed to fetch tokens');
|
|
465
|
-
}
|
|
466
|
-
const tokens = await response.json();
|
|
467
|
-
await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_verifyTokens).call(this, tokens);
|
|
468
|
-
return this.getProfile();
|
|
468
|
+
const formBody = new URLSearchParams();
|
|
469
|
+
formBody.set('grant_type', OAuthGrantType.TokenExchange);
|
|
470
|
+
formBody.set('subject_token_type', OAuthTokenType.AccessToken);
|
|
471
|
+
formBody.set('client_id', clientId);
|
|
472
|
+
formBody.set('subject_token', token);
|
|
473
|
+
return this.fetchTokens(formBody);
|
|
469
474
|
}
|
|
470
475
|
/**
|
|
471
476
|
* Builds the external identity provider redirect URI.
|
|
@@ -493,7 +498,7 @@ class MedplumClient extends EventTarget {
|
|
|
493
498
|
* @returns The well-formed FHIR URL.
|
|
494
499
|
*/
|
|
495
500
|
fhirUrl(...path) {
|
|
496
|
-
return new URL(
|
|
501
|
+
return new URL(this.fhirBaseUrl + path.join('/'));
|
|
497
502
|
}
|
|
498
503
|
/**
|
|
499
504
|
* Builds a FHIR search URL from a search query or structured query object.
|
|
@@ -561,7 +566,7 @@ class MedplumClient extends EventTarget {
|
|
|
561
566
|
search(resourceType, query, options = {}) {
|
|
562
567
|
const url = this.fhirSearchUrl(resourceType, query);
|
|
563
568
|
const cacheKey = url.toString() + '-search';
|
|
564
|
-
const cached =
|
|
569
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
565
570
|
if (cached) {
|
|
566
571
|
return cached.value;
|
|
567
572
|
}
|
|
@@ -569,12 +574,12 @@ class MedplumClient extends EventTarget {
|
|
|
569
574
|
const bundle = await this.get(url, options);
|
|
570
575
|
if (bundle.entry) {
|
|
571
576
|
for (const entry of bundle.entry) {
|
|
572
|
-
|
|
577
|
+
this.cacheResource(entry.resource);
|
|
573
578
|
}
|
|
574
579
|
}
|
|
575
580
|
return bundle;
|
|
576
581
|
})());
|
|
577
|
-
|
|
582
|
+
this.setCacheEntry(cacheKey, promise);
|
|
578
583
|
return promise;
|
|
579
584
|
}
|
|
580
585
|
/**
|
|
@@ -604,12 +609,12 @@ class MedplumClient extends EventTarget {
|
|
|
604
609
|
url.searchParams.set('_count', '1');
|
|
605
610
|
url.searchParams.sort();
|
|
606
611
|
const cacheKey = url.toString() + '-searchOne';
|
|
607
|
-
const cached =
|
|
612
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
608
613
|
if (cached) {
|
|
609
614
|
return cached.value;
|
|
610
615
|
}
|
|
611
616
|
const promise = new ReadablePromise(this.search(resourceType, url.searchParams, options).then((b) => b.entry?.[0]?.resource));
|
|
612
|
-
|
|
617
|
+
this.setCacheEntry(cacheKey, promise);
|
|
613
618
|
return promise;
|
|
614
619
|
}
|
|
615
620
|
/**
|
|
@@ -637,12 +642,12 @@ class MedplumClient extends EventTarget {
|
|
|
637
642
|
searchResources(resourceType, query, options = {}) {
|
|
638
643
|
const url = this.fhirSearchUrl(resourceType, query);
|
|
639
644
|
const cacheKey = url.toString() + '-searchResources';
|
|
640
|
-
const cached =
|
|
645
|
+
const cached = this.getCacheEntry(cacheKey, options);
|
|
641
646
|
if (cached) {
|
|
642
647
|
return cached.value;
|
|
643
648
|
}
|
|
644
649
|
const promise = new ReadablePromise(this.search(resourceType, query, options).then((b) => b.entry?.map((e) => e.resource) ?? []));
|
|
645
|
-
|
|
650
|
+
this.setCacheEntry(cacheKey, promise);
|
|
646
651
|
return promise;
|
|
647
652
|
}
|
|
648
653
|
/**
|
|
@@ -703,7 +708,7 @@ class MedplumClient extends EventTarget {
|
|
|
703
708
|
* @returns The resource if it is available in the cache; undefined otherwise.
|
|
704
709
|
*/
|
|
705
710
|
getCached(resourceType, id) {
|
|
706
|
-
const cached =
|
|
711
|
+
const cached = this.requestCache?.get(this.fhirUrl(resourceType, id).toString())?.value;
|
|
707
712
|
return cached && cached.isOk() ? cached.read() : undefined;
|
|
708
713
|
}
|
|
709
714
|
/**
|
|
@@ -805,7 +810,7 @@ class MedplumClient extends EventTarget {
|
|
|
805
810
|
return Promise.resolve(globalSchema);
|
|
806
811
|
}
|
|
807
812
|
const cacheKey = resourceType + '-requestSchema';
|
|
808
|
-
const cached =
|
|
813
|
+
const cached = this.getCacheEntry(cacheKey, undefined);
|
|
809
814
|
if (cached) {
|
|
810
815
|
return cached.value;
|
|
811
816
|
}
|
|
@@ -848,7 +853,7 @@ class MedplumClient extends EventTarget {
|
|
|
848
853
|
}
|
|
849
854
|
return globalSchema;
|
|
850
855
|
})());
|
|
851
|
-
|
|
856
|
+
this.setCacheEntry(cacheKey, promise);
|
|
852
857
|
return promise;
|
|
853
858
|
}
|
|
854
859
|
/**
|
|
@@ -1046,7 +1051,7 @@ class MedplumClient extends EventTarget {
|
|
|
1046
1051
|
};
|
|
1047
1052
|
xhr.open('POST', url);
|
|
1048
1053
|
xhr.withCredentials = true;
|
|
1049
|
-
xhr.setRequestHeader('Authorization', 'Bearer ' +
|
|
1054
|
+
xhr.setRequestHeader('Authorization', 'Bearer ' + this.accessToken);
|
|
1050
1055
|
xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, max-age=0');
|
|
1051
1056
|
xhr.setRequestHeader('Content-Type', contentType);
|
|
1052
1057
|
xhr.setRequestHeader('X-Medplum', 'extended');
|
|
@@ -1076,10 +1081,10 @@ class MedplumClient extends EventTarget {
|
|
|
1076
1081
|
* @returns The result of the create operation.
|
|
1077
1082
|
*/
|
|
1078
1083
|
async createPdf(docDefinition, filename, tableLayouts, fonts) {
|
|
1079
|
-
if (!
|
|
1084
|
+
if (!this.createPdfImpl) {
|
|
1080
1085
|
throw new Error('PDF creation not enabled');
|
|
1081
1086
|
}
|
|
1082
|
-
const blob = await
|
|
1087
|
+
const blob = await this.createPdfImpl(docDefinition, tableLayouts, fonts);
|
|
1083
1088
|
return this.createBinary(blob, filename, 'application/pdf');
|
|
1084
1089
|
}
|
|
1085
1090
|
/**
|
|
@@ -1157,7 +1162,7 @@ class MedplumClient extends EventTarget {
|
|
|
1157
1162
|
// return result ?? resource;
|
|
1158
1163
|
result = resource;
|
|
1159
1164
|
}
|
|
1160
|
-
|
|
1165
|
+
this.cacheResource(result);
|
|
1161
1166
|
return result;
|
|
1162
1167
|
}
|
|
1163
1168
|
/**
|
|
@@ -1205,7 +1210,7 @@ class MedplumClient extends EventTarget {
|
|
|
1205
1210
|
* @returns The result of the delete operation.
|
|
1206
1211
|
*/
|
|
1207
1212
|
deleteResource(resourceType, id) {
|
|
1208
|
-
|
|
1213
|
+
this.deleteCacheEntry(this.fhirUrl(resourceType, id).toString());
|
|
1209
1214
|
this.invalidateSearches(resourceType);
|
|
1210
1215
|
return this.delete(this.fhirUrl(resourceType, id));
|
|
1211
1216
|
}
|
|
@@ -1297,7 +1302,7 @@ class MedplumClient extends EventTarget {
|
|
|
1297
1302
|
* @returns The FHIR batch/transaction response bundle.
|
|
1298
1303
|
*/
|
|
1299
1304
|
executeBatch(bundle) {
|
|
1300
|
-
return this.post(
|
|
1305
|
+
return this.post(this.fhirBaseUrl.slice(0, -1), bundle);
|
|
1301
1306
|
}
|
|
1302
1307
|
/**
|
|
1303
1308
|
* Sends an email using the Medplum Email API.
|
|
@@ -1409,61 +1414,80 @@ class MedplumClient extends EventTarget {
|
|
|
1409
1414
|
* @returns The Login State
|
|
1410
1415
|
*/
|
|
1411
1416
|
getActiveLogin() {
|
|
1412
|
-
return
|
|
1417
|
+
return this.storage.getObject('activeLogin');
|
|
1413
1418
|
}
|
|
1414
1419
|
/**
|
|
1415
1420
|
* @category Authentication
|
|
1416
1421
|
*/
|
|
1417
1422
|
async setActiveLogin(login) {
|
|
1418
1423
|
this.clearActiveLogin();
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
await
|
|
1424
|
+
this.accessToken = login.accessToken;
|
|
1425
|
+
this.refreshToken = login.refreshToken;
|
|
1426
|
+
this.storage.setObject('activeLogin', login);
|
|
1427
|
+
this.addLogin(login);
|
|
1428
|
+
this.refreshPromise = undefined;
|
|
1429
|
+
await this.refreshProfile();
|
|
1425
1430
|
}
|
|
1426
1431
|
/**
|
|
1427
1432
|
* Returns the current access token.
|
|
1428
1433
|
* @category Authentication
|
|
1429
1434
|
*/
|
|
1430
1435
|
getAccessToken() {
|
|
1431
|
-
return
|
|
1436
|
+
return this.accessToken;
|
|
1432
1437
|
}
|
|
1433
1438
|
/**
|
|
1434
1439
|
* Sets the current access token.
|
|
1435
1440
|
* @category Authentication
|
|
1436
1441
|
*/
|
|
1437
1442
|
setAccessToken(accessToken) {
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1443
|
+
this.accessToken = accessToken;
|
|
1444
|
+
this.refreshToken = undefined;
|
|
1445
|
+
this.profile = undefined;
|
|
1446
|
+
this.config = undefined;
|
|
1442
1447
|
}
|
|
1443
1448
|
/**
|
|
1444
1449
|
* @category Authentication
|
|
1445
1450
|
*/
|
|
1446
1451
|
getLogins() {
|
|
1447
|
-
return
|
|
1452
|
+
return this.storage.getObject('logins') ?? [];
|
|
1453
|
+
}
|
|
1454
|
+
addLogin(newLogin) {
|
|
1455
|
+
const logins = this.getLogins().filter((login) => login.profile?.reference !== newLogin.profile?.reference);
|
|
1456
|
+
logins.push(newLogin);
|
|
1457
|
+
this.storage.setObject('logins', logins);
|
|
1458
|
+
}
|
|
1459
|
+
async refreshProfile() {
|
|
1460
|
+
this.profilePromise = new Promise((resolve, reject) => {
|
|
1461
|
+
this.get('auth/me')
|
|
1462
|
+
.then((result) => {
|
|
1463
|
+
this.profilePromise = undefined;
|
|
1464
|
+
this.profile = result.profile;
|
|
1465
|
+
this.config = result.config;
|
|
1466
|
+
this.dispatchEvent({ type: 'change' });
|
|
1467
|
+
resolve(this.profile);
|
|
1468
|
+
})
|
|
1469
|
+
.catch(reject);
|
|
1470
|
+
});
|
|
1471
|
+
return this.profilePromise;
|
|
1448
1472
|
}
|
|
1449
1473
|
/**
|
|
1450
1474
|
* @category Authentication
|
|
1451
1475
|
*/
|
|
1452
1476
|
isLoading() {
|
|
1453
|
-
return !!
|
|
1477
|
+
return !!this.profilePromise;
|
|
1454
1478
|
}
|
|
1455
1479
|
/**
|
|
1456
1480
|
* @category User Profile
|
|
1457
1481
|
*/
|
|
1458
1482
|
getProfile() {
|
|
1459
|
-
return
|
|
1483
|
+
return this.profile;
|
|
1460
1484
|
}
|
|
1461
1485
|
/**
|
|
1462
1486
|
* @category User Profile
|
|
1463
1487
|
*/
|
|
1464
1488
|
async getProfileAsync() {
|
|
1465
|
-
if (
|
|
1466
|
-
await
|
|
1489
|
+
if (this.profilePromise) {
|
|
1490
|
+
await this.profilePromise;
|
|
1467
1491
|
}
|
|
1468
1492
|
return this.getProfile();
|
|
1469
1493
|
}
|
|
@@ -1471,7 +1495,7 @@ class MedplumClient extends EventTarget {
|
|
|
1471
1495
|
* @category User Profile
|
|
1472
1496
|
*/
|
|
1473
1497
|
getUserConfiguration() {
|
|
1474
|
-
return
|
|
1498
|
+
return this.config;
|
|
1475
1499
|
}
|
|
1476
1500
|
/**
|
|
1477
1501
|
* Downloads the URL as a blob.
|
|
@@ -1481,13 +1505,234 @@ class MedplumClient extends EventTarget {
|
|
|
1481
1505
|
* @returns Promise to the response body as a blob.
|
|
1482
1506
|
*/
|
|
1483
1507
|
async download(url, options = {}) {
|
|
1484
|
-
if (
|
|
1485
|
-
await
|
|
1508
|
+
if (this.refreshPromise) {
|
|
1509
|
+
await this.refreshPromise;
|
|
1486
1510
|
}
|
|
1487
|
-
|
|
1488
|
-
const response = await
|
|
1511
|
+
this.addFetchOptionsDefaults(options);
|
|
1512
|
+
const response = await this.fetch(url.toString(), options);
|
|
1489
1513
|
return response.blob();
|
|
1490
1514
|
}
|
|
1515
|
+
//
|
|
1516
|
+
// Private helpers
|
|
1517
|
+
//
|
|
1518
|
+
/**
|
|
1519
|
+
* Returns the cache entry if available and not expired.
|
|
1520
|
+
* @param key The cache key to retrieve.
|
|
1521
|
+
* @param options Optional fetch options for cache settings.
|
|
1522
|
+
* @returns The cached entry if found.
|
|
1523
|
+
*/
|
|
1524
|
+
getCacheEntry(key, options) {
|
|
1525
|
+
if (!this.requestCache || options?.cache === 'no-cache' || options?.cache === 'reload') {
|
|
1526
|
+
return undefined;
|
|
1527
|
+
}
|
|
1528
|
+
const entry = this.requestCache.get(key);
|
|
1529
|
+
if (!entry || entry.requestTime + this.cacheTime < Date.now()) {
|
|
1530
|
+
return undefined;
|
|
1531
|
+
}
|
|
1532
|
+
return entry;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Adds a readable promise to the cache.
|
|
1536
|
+
* @param key The cache key to store.
|
|
1537
|
+
* @param value The readable promise to store.
|
|
1538
|
+
*/
|
|
1539
|
+
setCacheEntry(key, value) {
|
|
1540
|
+
if (this.requestCache) {
|
|
1541
|
+
this.requestCache.set(key, { requestTime: Date.now(), value });
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Adds a concrete value as the cache entry for the given resource.
|
|
1546
|
+
* This is used in cases where the resource is loaded indirectly.
|
|
1547
|
+
* For example, when a resource is loaded as part of a Bundle.
|
|
1548
|
+
* @param resource The resource to cache.
|
|
1549
|
+
*/
|
|
1550
|
+
cacheResource(resource) {
|
|
1551
|
+
if (resource?.id) {
|
|
1552
|
+
this.setCacheEntry(this.fhirUrl(resource.resourceType, resource.id).toString(), new ReadablePromise(Promise.resolve(resource)));
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Deletes a cache entry.
|
|
1557
|
+
* @param key The cache key to delete.
|
|
1558
|
+
*/
|
|
1559
|
+
deleteCacheEntry(key) {
|
|
1560
|
+
if (this.requestCache) {
|
|
1561
|
+
this.requestCache.delete(key);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Makes an HTTP request.
|
|
1566
|
+
* @param {string} method
|
|
1567
|
+
* @param {string} url
|
|
1568
|
+
* @param {string=} contentType
|
|
1569
|
+
* @param {Object=} body
|
|
1570
|
+
*/
|
|
1571
|
+
async request(method, url, options = {}) {
|
|
1572
|
+
if (this.refreshPromise) {
|
|
1573
|
+
await this.refreshPromise;
|
|
1574
|
+
}
|
|
1575
|
+
if (!url.startsWith('http')) {
|
|
1576
|
+
url = this.baseUrl + url;
|
|
1577
|
+
}
|
|
1578
|
+
options.method = method;
|
|
1579
|
+
this.addFetchOptionsDefaults(options);
|
|
1580
|
+
const response = await this.fetchWithRetry(url, options);
|
|
1581
|
+
if (response.status === 401) {
|
|
1582
|
+
// Refresh and try again
|
|
1583
|
+
return this.handleUnauthenticated(method, url, options);
|
|
1584
|
+
}
|
|
1585
|
+
if (response.status === 204 || response.status === 304) {
|
|
1586
|
+
// No content or change
|
|
1587
|
+
return undefined;
|
|
1588
|
+
}
|
|
1589
|
+
let obj = undefined;
|
|
1590
|
+
try {
|
|
1591
|
+
obj = await response.json();
|
|
1592
|
+
}
|
|
1593
|
+
catch (err) {
|
|
1594
|
+
console.error('Error parsing response', response.status, err);
|
|
1595
|
+
throw err;
|
|
1596
|
+
}
|
|
1597
|
+
if (response.status >= 400) {
|
|
1598
|
+
throw new OperationOutcomeError(normalizeOperationOutcome(obj));
|
|
1599
|
+
}
|
|
1600
|
+
return obj;
|
|
1601
|
+
}
|
|
1602
|
+
async fetchWithRetry(url, options) {
|
|
1603
|
+
const maxRetries = 3;
|
|
1604
|
+
const retryDelay = 200;
|
|
1605
|
+
let response = undefined;
|
|
1606
|
+
for (let retry = 0; retry < maxRetries; retry++) {
|
|
1607
|
+
response = (await this.fetch(url, options));
|
|
1608
|
+
if (response.status < 500) {
|
|
1609
|
+
return response;
|
|
1610
|
+
}
|
|
1611
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1612
|
+
}
|
|
1613
|
+
return response;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Executes a batch of requests that were automatically batched together.
|
|
1617
|
+
*/
|
|
1618
|
+
async executeAutoBatch() {
|
|
1619
|
+
// Get the current queue
|
|
1620
|
+
const entries = [...this.autoBatchQueue];
|
|
1621
|
+
// Clear the queue
|
|
1622
|
+
this.autoBatchQueue.length = 0;
|
|
1623
|
+
// Clear the timer
|
|
1624
|
+
this.autoBatchTimerId = undefined;
|
|
1625
|
+
// If there is only one request in the batch, just execute it
|
|
1626
|
+
if (entries.length === 1) {
|
|
1627
|
+
const entry = entries[0];
|
|
1628
|
+
try {
|
|
1629
|
+
entry.resolve(await this.request(entry.method, this.fhirBaseUrl + entry.url, entry.options));
|
|
1630
|
+
}
|
|
1631
|
+
catch (err) {
|
|
1632
|
+
entry.reject(new OperationOutcomeError(normalizeOperationOutcome(err)));
|
|
1633
|
+
}
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
// Build the batch request
|
|
1637
|
+
const batch = {
|
|
1638
|
+
resourceType: 'Bundle',
|
|
1639
|
+
type: 'batch',
|
|
1640
|
+
entry: entries.map((e) => ({
|
|
1641
|
+
request: {
|
|
1642
|
+
method: e.method,
|
|
1643
|
+
url: e.url,
|
|
1644
|
+
},
|
|
1645
|
+
resource: e.options.body ? JSON.parse(e.options.body) : undefined,
|
|
1646
|
+
})),
|
|
1647
|
+
};
|
|
1648
|
+
// Execute the batch request
|
|
1649
|
+
const response = (await this.post(this.fhirBaseUrl.slice(0, -1), batch));
|
|
1650
|
+
// Process the response
|
|
1651
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1652
|
+
const entry = entries[i];
|
|
1653
|
+
const responseEntry = response.entry?.[i];
|
|
1654
|
+
if (responseEntry?.response?.outcome && !isOk(responseEntry.response.outcome)) {
|
|
1655
|
+
entry.reject(new OperationOutcomeError(responseEntry.response.outcome));
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
entry.resolve(responseEntry?.resource);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Adds default options to the fetch options.
|
|
1664
|
+
* @param options The options to add defaults to.
|
|
1665
|
+
*/
|
|
1666
|
+
addFetchOptionsDefaults(options) {
|
|
1667
|
+
let headers = options.headers;
|
|
1668
|
+
if (!headers) {
|
|
1669
|
+
headers = {};
|
|
1670
|
+
options.headers = headers;
|
|
1671
|
+
}
|
|
1672
|
+
headers['X-Medplum'] = 'extended';
|
|
1673
|
+
if (options.body && !headers['Content-Type']) {
|
|
1674
|
+
headers['Content-Type'] = FHIR_CONTENT_TYPE;
|
|
1675
|
+
}
|
|
1676
|
+
if (this.accessToken) {
|
|
1677
|
+
headers['Authorization'] = 'Bearer ' + this.accessToken;
|
|
1678
|
+
}
|
|
1679
|
+
if (this.basicAuth) {
|
|
1680
|
+
headers['Authorization'] = 'Basic ' + this.basicAuth;
|
|
1681
|
+
}
|
|
1682
|
+
if (!options.cache) {
|
|
1683
|
+
options.cache = 'no-cache';
|
|
1684
|
+
}
|
|
1685
|
+
if (!options.credentials) {
|
|
1686
|
+
options.credentials = 'include';
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Sets the "Content-Type" header on fetch options.
|
|
1691
|
+
* @param options The fetch options.
|
|
1692
|
+
* @param contentType The new content type to set.
|
|
1693
|
+
*/
|
|
1694
|
+
setRequestContentType(options, contentType) {
|
|
1695
|
+
if (!options.headers) {
|
|
1696
|
+
options.headers = {};
|
|
1697
|
+
}
|
|
1698
|
+
const headers = options.headers;
|
|
1699
|
+
headers['Content-Type'] = contentType;
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Sets the body on fetch options.
|
|
1703
|
+
* @param options The fetch options.
|
|
1704
|
+
* @param data The new content body.
|
|
1705
|
+
*/
|
|
1706
|
+
setRequestBody(options, data) {
|
|
1707
|
+
if (typeof data === 'string' ||
|
|
1708
|
+
(typeof Blob !== 'undefined' && data instanceof Blob) ||
|
|
1709
|
+
(typeof File !== 'undefined' && data instanceof File) ||
|
|
1710
|
+
(typeof Uint8Array !== 'undefined' && data instanceof Uint8Array)) {
|
|
1711
|
+
options.body = data;
|
|
1712
|
+
}
|
|
1713
|
+
else if (data) {
|
|
1714
|
+
options.body = JSON.stringify(data);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Handles an unauthenticated response from the server.
|
|
1719
|
+
* First, tries to refresh the access token and retry the request.
|
|
1720
|
+
* Otherwise, calls unauthenticated callbacks and rejects.
|
|
1721
|
+
* @param method The HTTP method of the original request.
|
|
1722
|
+
* @param url The URL of the original request.
|
|
1723
|
+
* @param contentType The content type of the original request.
|
|
1724
|
+
* @param body The body of the original request.
|
|
1725
|
+
*/
|
|
1726
|
+
handleUnauthenticated(method, url, options) {
|
|
1727
|
+
if (this.refresh()) {
|
|
1728
|
+
return this.request(method, url, options);
|
|
1729
|
+
}
|
|
1730
|
+
this.clearActiveLogin();
|
|
1731
|
+
if (this.onUnauthenticated) {
|
|
1732
|
+
this.onUnauthenticated();
|
|
1733
|
+
}
|
|
1734
|
+
return Promise.reject(new Error('Unauthenticated'));
|
|
1735
|
+
}
|
|
1491
1736
|
/**
|
|
1492
1737
|
* Starts a new PKCE flow.
|
|
1493
1738
|
* These PKCE values are stateful, and must survive redirects and page refreshes.
|
|
@@ -1503,6 +1748,23 @@ class MedplumClient extends EventTarget {
|
|
|
1503
1748
|
sessionStorage.setItem('codeChallenge', codeChallenge);
|
|
1504
1749
|
return { codeChallengeMethod: 'S256', codeChallenge };
|
|
1505
1750
|
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Redirects the user to the login screen for authorization.
|
|
1753
|
+
* Clears all auth state including local storage and session storage.
|
|
1754
|
+
* See: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
|
1755
|
+
*/
|
|
1756
|
+
async requestAuthorization(loginParams) {
|
|
1757
|
+
const loginRequest = await this.ensureCodeChallenge(loginParams || {});
|
|
1758
|
+
const url = new URL(this.authorizeUrl);
|
|
1759
|
+
url.searchParams.set('response_type', 'code');
|
|
1760
|
+
url.searchParams.set('state', sessionStorage.getItem('pkceState'));
|
|
1761
|
+
url.searchParams.set('client_id', loginRequest.clientId || this.clientId);
|
|
1762
|
+
url.searchParams.set('redirect_uri', loginRequest.redirectUri || getWindowOrigin());
|
|
1763
|
+
url.searchParams.set('code_challenge_method', loginRequest.codeChallengeMethod);
|
|
1764
|
+
url.searchParams.set('code_challenge', loginRequest.codeChallenge);
|
|
1765
|
+
url.searchParams.set('scope', loginRequest.scope || 'openid profile');
|
|
1766
|
+
window.location.assign(url.toString());
|
|
1767
|
+
}
|
|
1506
1768
|
/**
|
|
1507
1769
|
* Processes an OAuth authorization code.
|
|
1508
1770
|
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
|
@@ -1512,9 +1774,9 @@ class MedplumClient extends EventTarget {
|
|
|
1512
1774
|
*/
|
|
1513
1775
|
processCode(code, loginParams) {
|
|
1514
1776
|
const formBody = new URLSearchParams();
|
|
1515
|
-
formBody.set('grant_type',
|
|
1777
|
+
formBody.set('grant_type', OAuthGrantType.AuthorizationCode);
|
|
1516
1778
|
formBody.set('code', code);
|
|
1517
|
-
formBody.set('client_id', loginParams?.clientId ||
|
|
1779
|
+
formBody.set('client_id', loginParams?.clientId || this.clientId);
|
|
1518
1780
|
formBody.set('redirect_uri', loginParams?.redirectUri || getWindowOrigin());
|
|
1519
1781
|
if (typeof sessionStorage !== 'undefined') {
|
|
1520
1782
|
const codeVerifier = sessionStorage.getItem('codeVerifier');
|
|
@@ -1522,7 +1784,29 @@ class MedplumClient extends EventTarget {
|
|
|
1522
1784
|
formBody.set('code_verifier', codeVerifier);
|
|
1523
1785
|
}
|
|
1524
1786
|
}
|
|
1525
|
-
return
|
|
1787
|
+
return this.fetchTokens(formBody);
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Tries to refresh the auth tokens.
|
|
1791
|
+
* See: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
|
1792
|
+
*/
|
|
1793
|
+
refresh() {
|
|
1794
|
+
if (this.refreshPromise) {
|
|
1795
|
+
return this.refreshPromise;
|
|
1796
|
+
}
|
|
1797
|
+
if (this.refreshToken) {
|
|
1798
|
+
const formBody = new URLSearchParams();
|
|
1799
|
+
formBody.set('grant_type', OAuthGrantType.RefreshToken);
|
|
1800
|
+
formBody.set('client_id', this.clientId);
|
|
1801
|
+
formBody.set('refresh_token', this.refreshToken);
|
|
1802
|
+
this.refreshPromise = this.fetchTokens(formBody);
|
|
1803
|
+
return this.refreshPromise;
|
|
1804
|
+
}
|
|
1805
|
+
if (this.clientId && this.clientSecret) {
|
|
1806
|
+
this.refreshPromise = this.startClientLogin(this.clientId, this.clientSecret);
|
|
1807
|
+
return this.refreshPromise;
|
|
1808
|
+
}
|
|
1809
|
+
return undefined;
|
|
1526
1810
|
}
|
|
1527
1811
|
/**
|
|
1528
1812
|
* Starts a new OAuth2 client credentials flow.
|
|
@@ -1533,13 +1817,24 @@ class MedplumClient extends EventTarget {
|
|
|
1533
1817
|
* @returns Promise that resolves to the client profile.
|
|
1534
1818
|
*/
|
|
1535
1819
|
async startClientLogin(clientId, clientSecret) {
|
|
1536
|
-
|
|
1537
|
-
|
|
1820
|
+
this.clientId = clientId;
|
|
1821
|
+
this.clientSecret = clientSecret;
|
|
1538
1822
|
const formBody = new URLSearchParams();
|
|
1539
|
-
formBody.set('grant_type',
|
|
1823
|
+
formBody.set('grant_type', OAuthGrantType.ClientCredentials);
|
|
1540
1824
|
formBody.set('client_id', clientId);
|
|
1541
1825
|
formBody.set('client_secret', clientSecret);
|
|
1542
|
-
return
|
|
1826
|
+
return this.fetchTokens(formBody);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Sets the client ID and secret for basic auth.
|
|
1830
|
+
* @category Authentication
|
|
1831
|
+
* @param clientId The client ID.
|
|
1832
|
+
* @param clientSecret The client secret.
|
|
1833
|
+
*/
|
|
1834
|
+
setBasicAuth(clientId, clientSecret) {
|
|
1835
|
+
this.clientId = clientId;
|
|
1836
|
+
this.clientSecret = clientSecret;
|
|
1837
|
+
this.basicAuth = encodeBase64(clientId + ':' + clientSecret);
|
|
1543
1838
|
}
|
|
1544
1839
|
/**
|
|
1545
1840
|
* Invite a user to a project.
|
|
@@ -1550,280 +1845,72 @@ class MedplumClient extends EventTarget {
|
|
|
1550
1845
|
async invite(projectId, body) {
|
|
1551
1846
|
return this.post('admin/projects/' + projectId + '/invite', body);
|
|
1552
1847
|
}
|
|
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));
|
|
1848
|
+
/**
|
|
1849
|
+
* Makes a POST request to the tokens endpoint.
|
|
1850
|
+
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
1851
|
+
* @param formBody Token parameters in URL encoded format.
|
|
1852
|
+
*/
|
|
1853
|
+
async fetchTokens(formBody) {
|
|
1854
|
+
const response = await this.fetch(this.tokenUrl, {
|
|
1855
|
+
method: 'POST',
|
|
1856
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1857
|
+
body: formBody,
|
|
1858
|
+
credentials: 'include',
|
|
1859
|
+
});
|
|
1860
|
+
if (!response.ok) {
|
|
1861
|
+
this.clearActiveLogin();
|
|
1862
|
+
throw new Error('Failed to fetch tokens');
|
|
1863
|
+
}
|
|
1864
|
+
const tokens = await response.json();
|
|
1865
|
+
await this.verifyTokens(tokens);
|
|
1866
|
+
return this.getProfile();
|
|
1628
1867
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1868
|
+
/**
|
|
1869
|
+
* Verifies the tokens received from the auth server.
|
|
1870
|
+
* Validates the JWT against the JWKS.
|
|
1871
|
+
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
1872
|
+
* @param tokens
|
|
1873
|
+
*/
|
|
1874
|
+
async verifyTokens(tokens) {
|
|
1875
|
+
const token = tokens.access_token;
|
|
1876
|
+
// Verify token has not expired
|
|
1877
|
+
const tokenPayload = parseJWTPayload(token);
|
|
1878
|
+
if (Date.now() >= tokenPayload.exp * 1000) {
|
|
1879
|
+
this.clearActiveLogin();
|
|
1880
|
+
throw new Error('Token expired');
|
|
1881
|
+
}
|
|
1882
|
+
// Verify app_client_id
|
|
1883
|
+
if (this.clientId && tokenPayload.client_id !== this.clientId) {
|
|
1884
|
+
this.clearActiveLogin();
|
|
1885
|
+
throw new Error('Token was not issued for this audience');
|
|
1638
1886
|
}
|
|
1639
|
-
|
|
1887
|
+
return this.setActiveLogin({
|
|
1888
|
+
accessToken: token,
|
|
1889
|
+
refreshToken: tokens.refresh_token,
|
|
1890
|
+
project: tokens.project,
|
|
1891
|
+
profile: tokens.profile,
|
|
1892
|
+
});
|
|
1640
1893
|
}
|
|
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];
|
|
1894
|
+
/**
|
|
1895
|
+
* Sets up a listener for window storage events.
|
|
1896
|
+
* This synchronizes state across browser windows and browser tabs.
|
|
1897
|
+
*/
|
|
1898
|
+
setupStorageListener() {
|
|
1656
1899
|
try {
|
|
1657
|
-
|
|
1900
|
+
window.addEventListener('storage', (e) => {
|
|
1901
|
+
if (e.key === null || e.key === 'activeLogin') {
|
|
1902
|
+
// Storage events fire when different tabs make changes.
|
|
1903
|
+
// On storage clear (key === null) or activeLogin change (key === 'activeLogin')
|
|
1904
|
+
// Refresh the page to ensure the active login is up to date.
|
|
1905
|
+
window.location.reload();
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1658
1908
|
}
|
|
1659
1909
|
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));
|
|
1910
|
+
// Silently ignore if this environment does not support storage events
|
|
1684
1911
|
}
|
|
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
1912
|
}
|
|
1711
|
-
|
|
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
|
-
}
|
|
1723
|
-
}, _MedplumClient_handleUnauthenticated = function _MedplumClient_handleUnauthenticated(method, url, options) {
|
|
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
|
-
};
|
|
1913
|
+
}
|
|
1827
1914
|
/**
|
|
1828
1915
|
* Returns the default fetch method.
|
|
1829
1916
|
* The default fetch is currently only available in browser environments.
|
|
@@ -1853,5 +1940,5 @@ function ensureTrailingSlash(url) {
|
|
|
1853
1940
|
return url.endsWith('/') ? url : url + '/';
|
|
1854
1941
|
}
|
|
1855
1942
|
|
|
1856
|
-
export { MEDPLUM_VERSION, MedplumClient };
|
|
1943
|
+
export { MEDPLUM_VERSION, MedplumClient, OAuthGrantType, OAuthTokenType };
|
|
1857
1944
|
//# sourceMappingURL=client.mjs.map
|