@medplum/core 1.0.6 → 2.0.0

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.
@@ -1,4 +1,4 @@
1
- import { CodeableConcept, ObservationDefinition, ObservationDefinitionQualifiedInterval, Patient, Practitioner, QuestionnaireResponse, QuestionnaireResponseItemAnswer, Range, Reference, RelatedPerson, Resource } from '@medplum/fhirtypes';
1
+ import { CodeableConcept, Extension, ObservationDefinition, ObservationDefinitionQualifiedInterval, Patient, Practitioner, QuestionnaireResponse, QuestionnaireResponseItemAnswer, Range, Reference, RelatedPerson, Resource } from '@medplum/fhirtypes';
2
2
  /**
3
3
  * @internal
4
4
  */
@@ -93,6 +93,13 @@ export declare function getIdentifier(resource: Resource, system: string): strin
93
93
  * @returns The extension value if found; undefined otherwise.
94
94
  */
95
95
  export declare function getExtensionValue(resource: any, ...urls: string[]): string | undefined;
96
+ /**
97
+ * Returns an extension by extension URLs.
98
+ * @param resource The base resource.
99
+ * @param urls Array of extension URLs. Each entry represents a nested extension.
100
+ * @returns The extension object if found; undefined otherwise.
101
+ */
102
+ export declare function getExtension(resource: any, ...urls: string[]): Extension | undefined;
96
103
  /**
97
104
  * FHIR JSON stringify.
98
105
  * Removes properties with empty string values.
@@ -146,6 +146,7 @@ export interface BaseLoginRequest {
146
146
  readonly codeChallengeMethod?: string;
147
147
  readonly googleClientId?: string;
148
148
  readonly launch?: string;
149
+ readonly redirectUri?: string;
149
150
  }
150
151
  export interface EmailPasswordLoginRequest extends BaseLoginRequest {
151
152
  readonly email: string;
@@ -338,6 +339,12 @@ export declare class MedplumClient extends EventTarget {
338
339
  * @category Authentication
339
340
  */
340
341
  clear(): void;
342
+ /**
343
+ * Clears the active login from local storage.
344
+ * Does not clear all local storage (such as other logins).
345
+ * @category Authentication
346
+ */
347
+ clearActiveLogin(): void;
341
348
  /**
342
349
  * Invalidates any cached values or cached requests for the given URL.
343
350
  * @category Caching
@@ -467,10 +474,15 @@ export declare class MedplumClient extends EventTarget {
467
474
  * @returns Promise to the authentication response.
468
475
  */
469
476
  startGoogleLogin(loginRequest: GoogleLoginRequest): Promise<LoginAuthenticationResponse>;
470
- getCodeChallenge(loginRequest: BaseLoginRequest): {
471
- codeChallenge?: string;
472
- codeChallengeMethod?: string;
473
- };
477
+ /**
478
+ * Returns the PKCE code challenge and method.
479
+ * If the login request already includes a code challenge, it is returned.
480
+ * Otherwise, a new PKCE code challenge is generated.
481
+ * @category Authentication
482
+ * @param loginRequest The original login request.
483
+ * @returns The PKCE code challenge and method.
484
+ */
485
+ ensureCodeChallenge<T extends BaseLoginRequest>(loginRequest: T): Promise<T>;
474
486
  /**
475
487
  * Signs out locally.
476
488
  * Does not invalidate tokens with the server.
@@ -482,8 +494,9 @@ export declare class MedplumClient extends EventTarget {
482
494
  * Returns true if the user is signed in.
483
495
  * This may result in navigating away to the sign in page.
484
496
  * @category Authentication
497
+ * @param loginParams Optional login parameters.
485
498
  */
486
- signInWithRedirect(): Promise<ProfileResource | void>;
499
+ signInWithRedirect(loginParams?: Partial<BaseLoginRequest>): Promise<ProfileResource | void>;
487
500
  /**
488
501
  * Tries to sign out the user.
489
502
  * See: https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html
@@ -1154,7 +1167,10 @@ export declare class MedplumClient extends EventTarget {
1154
1167
  * Starts a new PKCE flow.
1155
1168
  * These PKCE values are stateful, and must survive redirects and page refreshes.
1156
1169
  */
1157
- startPkce(): Promise<void>;
1170
+ startPkce(): Promise<{
1171
+ codeChallengeMethod: string;
1172
+ codeChallenge: string;
1173
+ }>;
1158
1174
  /**
1159
1175
  * Processes an OAuth authorization code.
1160
1176
  * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
@@ -11,7 +11,7 @@ import { createReference, arrayBufferToBase64 } from './utils.mjs';
11
11
  // PKCE auth based on:
12
12
  // https://aws.amazon.com/blogs/security/how-to-add-authentication-single-page-web-application-with-amazon-cognito-oauth2-implementation/
13
13
  var _MedplumClient_instances, _MedplumClient_fetch, _MedplumClient_createPdf, _MedplumClient_storage, _MedplumClient_requestCache, _MedplumClient_cacheTime, _MedplumClient_baseUrl, _MedplumClient_authorizeUrl, _MedplumClient_tokenUrl, _MedplumClient_logoutUrl, _MedplumClient_onUnauthenticated, _MedplumClient_clientId, _MedplumClient_clientSecret, _MedplumClient_accessToken, _MedplumClient_refreshToken, _MedplumClient_refreshPromise, _MedplumClient_profilePromise, _MedplumClient_profile, _MedplumClient_config, _MedplumClient_addLogin, _MedplumClient_refreshProfile, _MedplumClient_getCacheEntry, _MedplumClient_setCacheEntry, _MedplumClient_request, _MedplumClient_addFetchOptionsDefaults, _MedplumClient_setRequestContentType, _MedplumClient_setRequestBody, _MedplumClient_handleUnauthenticated, _MedplumClient_requestAuthorization, _MedplumClient_refresh, _MedplumClient_fetchTokens, _MedplumClient_verifyTokens, _MedplumClient_setupStorageListener;
14
- const MEDPLUM_VERSION = "1.0.6-5860113c";
14
+ const MEDPLUM_VERSION = "2.0.0-7fe99080";
15
15
  const DEFAULT_BASE_URL = 'https://api.medplum.com/';
16
16
  const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
17
17
  const DEFAULT_CACHE_TIME = 60000; // 60 seconds
@@ -132,6 +132,15 @@ class MedplumClient extends EventTarget {
132
132
  */
133
133
  clear() {
134
134
  __classPrivateFieldGet(this, _MedplumClient_storage, "f").clear();
135
+ this.clearActiveLogin();
136
+ }
137
+ /**
138
+ * Clears the active login from local storage.
139
+ * Does not clear all local storage (such as other logins).
140
+ * @category Authentication
141
+ */
142
+ clearActiveLogin() {
143
+ __classPrivateFieldGet(this, _MedplumClient_storage, "f").setString('activeLogin', undefined);
135
144
  __classPrivateFieldGet(this, _MedplumClient_requestCache, "f").clear();
136
145
  __classPrivateFieldSet(this, _MedplumClient_accessToken, undefined, "f");
137
146
  __classPrivateFieldSet(this, _MedplumClient_refreshToken, undefined, "f");
@@ -283,11 +292,11 @@ class MedplumClient extends EventTarget {
283
292
  * @returns Promise to the authentication response.
284
293
  */
285
294
  async startNewUser(newUserRequest) {
286
- await this.startPkce();
295
+ const { codeChallengeMethod, codeChallenge } = await this.startPkce();
287
296
  return this.post('auth/newuser', {
288
297
  ...newUserRequest,
289
- codeChallengeMethod: 'S256',
290
- codeChallenge: sessionStorage.getItem('codeChallenge'),
298
+ codeChallengeMethod,
299
+ codeChallenge,
291
300
  });
292
301
  }
293
302
  /**
@@ -319,13 +328,10 @@ class MedplumClient extends EventTarget {
319
328
  * @returns Promise to the authentication response.
320
329
  */
321
330
  async startLogin(loginRequest) {
322
- const { codeChallenge, codeChallengeMethod } = this.getCodeChallenge(loginRequest);
323
331
  return this.post('auth/login', {
324
- ...loginRequest,
332
+ ...(await this.ensureCodeChallenge(loginRequest)),
325
333
  clientId: loginRequest.clientId ?? __classPrivateFieldGet(this, _MedplumClient_clientId, "f"),
326
334
  scope: loginRequest.scope,
327
- codeChallengeMethod,
328
- codeChallenge,
329
335
  });
330
336
  }
331
337
  /**
@@ -337,30 +343,25 @@ class MedplumClient extends EventTarget {
337
343
  * @returns Promise to the authentication response.
338
344
  */
339
345
  async startGoogleLogin(loginRequest) {
340
- const { codeChallenge, codeChallengeMethod } = this.getCodeChallenge(loginRequest);
341
346
  return this.post('auth/google', {
342
- ...loginRequest,
347
+ ...(await this.ensureCodeChallenge(loginRequest)),
343
348
  clientId: loginRequest.clientId ?? __classPrivateFieldGet(this, _MedplumClient_clientId, "f"),
344
349
  scope: loginRequest.scope,
345
- codeChallengeMethod,
346
- codeChallenge,
347
350
  });
348
351
  }
349
- getCodeChallenge(loginRequest) {
352
+ /**
353
+ * Returns the PKCE code challenge and method.
354
+ * If the login request already includes a code challenge, it is returned.
355
+ * Otherwise, a new PKCE code challenge is generated.
356
+ * @category Authentication
357
+ * @param loginRequest The original login request.
358
+ * @returns The PKCE code challenge and method.
359
+ */
360
+ async ensureCodeChallenge(loginRequest) {
350
361
  if (loginRequest.codeChallenge) {
351
- return {
352
- codeChallenge: loginRequest.codeChallenge,
353
- codeChallengeMethod: loginRequest.codeChallengeMethod,
354
- };
355
- }
356
- const codeChallenge = sessionStorage.getItem('codeChallenge');
357
- if (codeChallenge) {
358
- return {
359
- codeChallenge,
360
- codeChallengeMethod: 'S256',
361
- };
362
+ return loginRequest;
362
363
  }
363
- return {};
364
+ return { ...loginRequest, ...(await this.startPkce()) };
364
365
  }
365
366
  /**
366
367
  * Signs out locally.
@@ -375,12 +376,13 @@ class MedplumClient extends EventTarget {
375
376
  * Returns true if the user is signed in.
376
377
  * This may result in navigating away to the sign in page.
377
378
  * @category Authentication
379
+ * @param loginParams Optional login parameters.
378
380
  */
379
- async signInWithRedirect() {
381
+ async signInWithRedirect(loginParams) {
380
382
  const urlParams = new URLSearchParams(window.location.search);
381
383
  const code = urlParams.get('code');
382
384
  if (!code) {
383
- await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_requestAuthorization).call(this);
385
+ await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_requestAuthorization).call(this, loginParams);
384
386
  return undefined;
385
387
  }
386
388
  else {
@@ -1233,13 +1235,11 @@ class MedplumClient extends EventTarget {
1233
1235
  * @category Authentication
1234
1236
  */
1235
1237
  async setActiveLogin(login) {
1238
+ this.clearActiveLogin();
1236
1239
  __classPrivateFieldSet(this, _MedplumClient_accessToken, login.accessToken, "f");
1237
1240
  __classPrivateFieldSet(this, _MedplumClient_refreshToken, login.refreshToken, "f");
1238
- __classPrivateFieldSet(this, _MedplumClient_profile, undefined, "f");
1239
- __classPrivateFieldSet(this, _MedplumClient_config, undefined, "f");
1240
1241
  __classPrivateFieldGet(this, _MedplumClient_storage, "f").setObject('activeLogin', login);
1241
1242
  __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_addLogin).call(this, login);
1242
- __classPrivateFieldGet(this, _MedplumClient_requestCache, "f").clear();
1243
1243
  __classPrivateFieldSet(this, _MedplumClient_refreshPromise, undefined, "f");
1244
1244
  await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_refreshProfile).call(this);
1245
1245
  }
@@ -1318,6 +1318,7 @@ class MedplumClient extends EventTarget {
1318
1318
  const arrayHash = await encryptSHA256(codeVerifier);
1319
1319
  const codeChallenge = arrayBufferToBase64(arrayHash).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
1320
1320
  sessionStorage.setItem('codeChallenge', codeChallenge);
1321
+ return { codeChallengeMethod: 'S256', codeChallenge };
1321
1322
  }
1322
1323
  /**
1323
1324
  * Processes an OAuth authorization code.
@@ -1453,7 +1454,7 @@ async function _MedplumClient_request(method, url, options = {}) {
1453
1454
  if (__classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_refresh).call(this)) {
1454
1455
  return __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_request).call(this, method, url, options);
1455
1456
  }
1456
- this.clear();
1457
+ this.clearActiveLogin();
1457
1458
  if (__classPrivateFieldGet(this, _MedplumClient_onUnauthenticated, "f")) {
1458
1459
  __classPrivateFieldGet(this, _MedplumClient_onUnauthenticated, "f").call(this);
1459
1460
  }
@@ -1464,15 +1465,16 @@ async function _MedplumClient_request(method, url, options = {}) {
1464
1465
  * Clears all auth state including local storage and session storage.
1465
1466
  * See: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
1466
1467
  */
1467
- async function _MedplumClient_requestAuthorization() {
1468
- await this.startPkce();
1468
+ async function _MedplumClient_requestAuthorization(loginParams) {
1469
+ const loginRequest = await this.ensureCodeChallenge(loginParams || {});
1469
1470
  const url = new URL(__classPrivateFieldGet(this, _MedplumClient_authorizeUrl, "f"));
1470
1471
  url.searchParams.set('response_type', 'code');
1471
1472
  url.searchParams.set('state', sessionStorage.getItem('pkceState'));
1472
- url.searchParams.set('client_id', __classPrivateFieldGet(this, _MedplumClient_clientId, "f"));
1473
- url.searchParams.set('redirect_uri', getBaseUrl());
1474
- url.searchParams.set('code_challenge_method', 'S256');
1475
- url.searchParams.set('code_challenge', sessionStorage.getItem('codeChallenge'));
1473
+ url.searchParams.set('client_id', loginRequest.clientId || __classPrivateFieldGet(this, _MedplumClient_clientId, "f"));
1474
+ url.searchParams.set('redirect_uri', loginRequest.redirectUri || getBaseUrl());
1475
+ url.searchParams.set('code_challenge_method', loginRequest.codeChallengeMethod);
1476
+ url.searchParams.set('code_challenge', loginRequest.codeChallenge);
1477
+ url.searchParams.set('scope', loginRequest.scope || 'openid profile');
1476
1478
  window.location.assign(url.toString());
1477
1479
  }, _MedplumClient_refresh = function _MedplumClient_refresh() {
1478
1480
  if (__classPrivateFieldGet(this, _MedplumClient_refreshPromise, "f")) {
@@ -1498,20 +1500,19 @@ async function _MedplumClient_requestAuthorization() {
1498
1500
  * @param formBody Token parameters in URL encoded format.
1499
1501
  */
1500
1502
  async function _MedplumClient_fetchTokens(formBody) {
1501
- return __classPrivateFieldGet(this, _MedplumClient_fetch, "f").call(this, __classPrivateFieldGet(this, _MedplumClient_tokenUrl, "f"), {
1503
+ const response = await __classPrivateFieldGet(this, _MedplumClient_fetch, "f").call(this, __classPrivateFieldGet(this, _MedplumClient_tokenUrl, "f"), {
1502
1504
  method: 'POST',
1503
1505
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1504
1506
  body: formBody,
1505
1507
  credentials: 'include',
1506
- })
1507
- .then((response) => {
1508
- if (!response.ok) {
1509
- throw new Error('Failed to fetch tokens');
1510
- }
1511
- return response.json();
1512
- })
1513
- .then((tokens) => __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_verifyTokens).call(this, tokens))
1514
- .then(() => this.getProfile());
1508
+ });
1509
+ if (!response.ok) {
1510
+ this.clearActiveLogin();
1511
+ throw new Error('Failed to fetch tokens');
1512
+ }
1513
+ const tokens = await response.json();
1514
+ await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_verifyTokens).call(this, tokens);
1515
+ return this.getProfile();
1515
1516
  }, _MedplumClient_verifyTokens =
1516
1517
  /**
1517
1518
  * Verifies the tokens received from the auth server.
@@ -1524,15 +1525,15 @@ async function _MedplumClient_verifyTokens(tokens) {
1524
1525
  // Verify token has not expired
1525
1526
  const tokenPayload = parseJWTPayload(token);
1526
1527
  if (Date.now() >= tokenPayload.exp * 1000) {
1527
- this.clear();
1528
+ this.clearActiveLogin();
1528
1529
  throw new Error('Token expired');
1529
1530
  }
1530
1531
  // Verify app_client_id
1531
1532
  if (__classPrivateFieldGet(this, _MedplumClient_clientId, "f") && tokenPayload.client_id !== __classPrivateFieldGet(this, _MedplumClient_clientId, "f")) {
1532
- this.clear();
1533
+ this.clearActiveLogin();
1533
1534
  throw new Error('Token was not issued for this audience');
1534
1535
  }
1535
- await this.setActiveLogin({
1536
+ return this.setActiveLogin({
1536
1537
  accessToken: token,
1537
1538
  refreshToken: tokens.refresh_token,
1538
1539
  project: tokens.project,