@ovok/core 0.4.36 → 0.4.38

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.
Files changed (127) hide show
  1. package/dist/client/auth/account-storage.d.ts +1 -0
  2. package/dist/client/auth/account-storage.js +2 -1
  3. package/dist/{utils → client}/auth/exchange-code.d.ts +1 -1
  4. package/dist/{utils → client}/auth/generate-code-challenge-and-verifier.js +5 -17
  5. package/dist/{utils → client}/auth/handle-tenant-code.d.ts +1 -1
  6. package/dist/{utils → client}/auth/handle-tenant-code.js +1 -1
  7. package/dist/client/auth/methods/appleLogin.js +2 -6
  8. package/dist/client/auth/methods/changePassword.js +2 -1
  9. package/dist/client/auth/methods/getSessions.js +2 -1
  10. package/dist/client/auth/methods/googleLogin.js +2 -6
  11. package/dist/client/auth/methods/login.js +26 -44
  12. package/dist/client/auth/methods/logout.js +2 -1
  13. package/dist/client/auth/methods/requestDeleteUser.js +5 -4
  14. package/dist/client/auth/methods/revokeSessions.js +2 -1
  15. package/dist/client/auth/methods/socialLogin.d.ts +7 -0
  16. package/dist/client/auth/methods/socialLogin.js +11 -0
  17. package/dist/client/auth/session-fetch.d.ts +35 -0
  18. package/dist/client/auth/session-fetch.js +206 -0
  19. package/dist/client/auth/session-http.d.ts +11 -0
  20. package/dist/client/auth/session-http.js +169 -0
  21. package/dist/client/auth/sha256.d.ts +2 -0
  22. package/dist/client/auth/sha256.js +65 -0
  23. package/dist/{utils → client}/auth/start-login.d.ts +2 -2
  24. package/dist/client/change-history/methods.js +2 -1
  25. package/dist/client/cms/methods.js +16 -28
  26. package/dist/client/consent/methods.js +3 -2
  27. package/dist/client/email/methods.js +2 -1
  28. package/dist/client/errors/ovok-error-utils.d.ts +6 -0
  29. package/dist/client/errors/ovok-error-utils.js +10 -1
  30. package/dist/client/errors/ovok-error.js +6 -13
  31. package/dist/client/errors/rate-limit-error.js +2 -6
  32. package/dist/client/experience/methods.js +28 -37
  33. package/dist/client/invites/methods.js +8 -12
  34. package/dist/client/notifications/methods.js +5 -5
  35. package/dist/client/observation/fhir-systems.d.ts +4 -0
  36. package/dist/client/observation/fhir-systems.js +7 -0
  37. package/dist/client/observation/methods/generateObservationBodiesByMeasurement.js +23 -29
  38. package/dist/client/observation/methods/getLatestObservationsByCodes.js +6 -9
  39. package/dist/client/observation/methods/observationsToMeasurements.js +44 -91
  40. package/dist/client/observation/services/base-observation-service.js +4 -3
  41. package/dist/client/observation/services/blood-pressure.js +1 -4
  42. package/dist/client/observation/services/{baby-sleep.d.ts → component-observation-services.d.ts} +18 -13
  43. package/dist/client/observation/services/component-observation-services.js +69 -0
  44. package/dist/client/observation/services/creator.d.ts +1 -2
  45. package/dist/client/observation/services/creator.js +15 -33
  46. package/dist/client/observation/services/ecg-observation-fragments.js +5 -6
  47. package/dist/client/observation/services/ecg.js +3 -2
  48. package/dist/client/observation/services/lipid-panel.js +8 -12
  49. package/dist/client/observation/services/measurement-time.d.ts +4 -0
  50. package/dist/client/observation/services/measurement-time.js +6 -0
  51. package/dist/client/observation/services/single-value-services.d.ts +36 -0
  52. package/dist/client/observation/services/single-value-services.js +100 -0
  53. package/dist/client/observation/services/uric-acid-service.js +6 -7
  54. package/dist/client/observation/types/generate-measurement/HealthConnectTypes.d.ts +2 -458
  55. package/dist/client/observation/types/generate-measurement/HealthConnectTypes.js +0 -6
  56. package/dist/client/offline/offline-measurement-queue.js +31 -28
  57. package/dist/client/ovok-client.d.ts +7 -15
  58. package/dist/client/ovok-client.js +54 -398
  59. package/dist/client/patient/methods.js +27 -36
  60. package/dist/client/patient-sharing/methods.js +4 -4
  61. package/dist/client/platform/methods.js +21 -30
  62. package/dist/client/platform/types.d.ts +3 -6
  63. package/dist/client/query-string.d.ts +3 -0
  64. package/dist/client/query-string.js +15 -0
  65. package/dist/client/questionnaire-response/methods/createQuestionnaireResponseWithObservations.js +2 -1
  66. package/dist/client/questionnaire-response/methods/extractQuestionnaireObservations.js +1 -2
  67. package/dist/client/questionnaire-response/methods/populateQuestionnaire.js +1 -2
  68. package/dist/client/questionnaire-response/methods/submitQuestionnaireResponse.js +2 -1
  69. package/dist/client/request-options.d.ts +2 -0
  70. package/dist/client/request-options.js +6 -0
  71. package/dist/client/signals/errors.js +2 -6
  72. package/dist/client/signals/methods.js +22 -22
  73. package/dist/client/slim/live-read.d.ts +4 -0
  74. package/dist/client/slim/live-read.js +87 -0
  75. package/dist/client/slim/methods.d.ts +3 -0
  76. package/dist/client/slim/methods.js +73 -166
  77. package/dist/client/storage.js +3 -2
  78. package/dist/conformance/capability-requirements.js +1 -1
  79. package/dist/conformance/capability.js +8 -7
  80. package/dist/hooks/account-hooks.js +6 -68
  81. package/dist/hooks/api-hooks.d.ts +10 -6
  82. package/dist/hooks/api-hooks.js +29 -88
  83. package/dist/hooks/cms-hooks.js +8 -13
  84. package/dist/hooks/email-hooks.js +1 -40
  85. package/dist/hooks/experience-hooks.js +4 -60
  86. package/dist/hooks/index.d.ts +2 -2
  87. package/dist/hooks/index.js +4 -4
  88. package/dist/hooks/invites-hooks.js +0 -1
  89. package/dist/hooks/notification-hooks.d.ts +4 -1
  90. package/dist/hooks/notification-hooks.js +29 -86
  91. package/dist/hooks/notification-invalidation.d.ts +6 -0
  92. package/dist/hooks/notification-invalidation.js +52 -0
  93. package/dist/hooks/notification-note-hooks.js +13 -28
  94. package/dist/hooks/observation-hooks.js +2 -4
  95. package/dist/hooks/patient-hooks.js +13 -113
  96. package/dist/hooks/patient-sharing-hooks.js +2 -7
  97. package/dist/hooks/platform-hooks.js +3 -51
  98. package/dist/hooks/poll-hooks.js +10 -8
  99. package/dist/hooks/signals-alert-hooks.js +4 -6
  100. package/dist/hooks/signals-hooks.js +2 -45
  101. package/package.json +1 -1
  102. package/dist/client/observation/services/baby-sleep.js +0 -69
  103. package/dist/client/observation/services/blood-glucose.d.ts +0 -12
  104. package/dist/client/observation/services/blood-glucose.js +0 -36
  105. package/dist/client/observation/services/body-temperature.d.ts +0 -13
  106. package/dist/client/observation/services/body-temperature.js +0 -36
  107. package/dist/client/observation/services/body-weight.d.ts +0 -13
  108. package/dist/client/observation/services/body-weight.js +0 -36
  109. package/dist/client/observation/services/heart-rate-variability.d.ts +0 -13
  110. package/dist/client/observation/services/heart-rate-variability.js +0 -38
  111. package/dist/client/observation/services/heart-rate.d.ts +0 -13
  112. package/dist/client/observation/services/heart-rate.js +0 -39
  113. package/dist/client/observation/services/respiratory-rate.d.ts +0 -13
  114. package/dist/client/observation/services/respiratory-rate.js +0 -38
  115. package/dist/client/observation/services/respiratory-therapy.d.ts +0 -28
  116. package/dist/client/observation/services/respiratory-therapy.js +0 -69
  117. package/dist/client/observation/services/resting-heart-rate.d.ts +0 -13
  118. package/dist/client/observation/services/resting-heart-rate.js +0 -38
  119. package/dist/client/observation/services/vo2-max.d.ts +0 -13
  120. package/dist/client/observation/services/vo2-max.js +0 -38
  121. package/dist/client/observation/services/walking-heart-rate-average.d.ts +0 -12
  122. package/dist/client/observation/services/walking-heart-rate-average.js +0 -38
  123. package/dist/utils/auth/sha256.d.ts +0 -8
  124. package/dist/utils/auth/sha256.js +0 -300
  125. /package/dist/{utils → client}/auth/exchange-code.js +0 -0
  126. /package/dist/{utils → client}/auth/generate-code-challenge-and-verifier.d.ts +0 -0
  127. /package/dist/{utils → client}/auth/start-login.js +0 -0
@@ -1,5 +1,6 @@
1
1
  import { IClientStorage } from "@medplum/core";
2
2
  export declare const LOGINS_STORAGE_KEY = "logins";
3
+ export declare const ACTIVE_LOGIN_STORAGE_KEY = "activeLogin";
3
4
  export declare const EXPIRED_ACCOUNTS_STORAGE_KEY = "ovok.core.expired-accounts";
4
5
  export declare const setStoredAccountExpired: (storage: IClientStorage, accountId: string, expired: boolean) => void;
5
6
  export declare const removeStoredAccount: (storage: IClientStorage, accountId: string) => void;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.removeStoredAccount = exports.setStoredAccountExpired = exports.EXPIRED_ACCOUNTS_STORAGE_KEY = exports.LOGINS_STORAGE_KEY = void 0;
3
+ exports.removeStoredAccount = exports.setStoredAccountExpired = exports.EXPIRED_ACCOUNTS_STORAGE_KEY = exports.ACTIVE_LOGIN_STORAGE_KEY = exports.LOGINS_STORAGE_KEY = void 0;
4
4
  exports.LOGINS_STORAGE_KEY = "logins";
5
+ exports.ACTIVE_LOGIN_STORAGE_KEY = "activeLogin";
5
6
  exports.EXPIRED_ACCOUNTS_STORAGE_KEY = "ovok.core.expired-accounts";
6
7
  const setStoredAccountExpired = (storage, accountId, expired) => {
7
8
  var _a;
@@ -1,6 +1,6 @@
1
1
  import type { MedplumRequestOptions } from "@medplum/core";
2
- import { OvokClient } from "../../client/ovok-client";
3
2
  import { AuthResponse } from "../../types";
3
+ import { OvokClient } from "../ovok-client";
4
4
  type ExchangeCodeProps = {
5
5
  client: OvokClient;
6
6
  sessionCode: string;
@@ -6,26 +6,16 @@ function generateRandomString(length) {
6
6
  const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
7
7
  let result = "";
8
8
  const randomValues = new Uint8Array(length);
9
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
10
- crypto.getRandomValues(randomValues);
11
- }
12
- else {
13
- for (let i = 0; i < length; i++) {
14
- randomValues[i] = Math.floor(Math.random() * 256);
15
- }
9
+ // A predictable verifier defeats PKCE, so there is no Math.random fallback.
10
+ if (typeof crypto === "undefined" || typeof crypto.getRandomValues !== "function") {
11
+ throw new Error("PKCE needs crypto.getRandomValues. On React Native, install a polyfill such as react-native-get-random-values (or use @ovok/native, which provides one).");
16
12
  }
13
+ crypto.getRandomValues(randomValues);
17
14
  for (let i = 0; i < length; i++) {
18
15
  result += charset[randomValues[i] % charset.length];
19
16
  }
20
17
  return result;
21
18
  }
22
- function hexToUint8Array(hex) {
23
- const bytes = new Uint8Array(hex.length / 2);
24
- for (let i = 0; i < hex.length; i += 2) {
25
- bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
26
- }
27
- return bytes;
28
- }
29
19
  function base64URLEncode(data) {
30
20
  let binary = "";
31
21
  for (let i = 0; i < data.length; i++) {
@@ -35,9 +25,7 @@ function base64URLEncode(data) {
35
25
  }
36
26
  function generateCodeChallengeAndVerifier() {
37
27
  const codeVerifier = generateRandomString(128);
38
- const hexHash = sha256_1.sha256.hex(codeVerifier);
39
- const hashBytes = hexToUint8Array(hexHash);
40
- const codeChallenge = base64URLEncode(hashBytes);
28
+ const codeChallenge = base64URLEncode((0, sha256_1.sha256Bytes)(codeVerifier));
41
29
  return {
42
30
  codeVerifier,
43
31
  codeChallenge,
@@ -1,4 +1,4 @@
1
- import { StartLoginResponse } from "../../client/auth/types/StartLoginResponse";
1
+ import { StartLoginResponse } from "./types/StartLoginResponse";
2
2
  export declare const handleTenantCode: ({ profiles, tenantCode, type, }: {
3
3
  profiles: StartLoginResponse["profiles"];
4
4
  tenantCode?: string;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleTenantCode = void 0;
4
- const LoginError_1 = require("../../client/auth/types/LoginError");
4
+ const LoginError_1 = require("./types/LoginError");
5
5
  const handleTenantCode = ({ profiles, tenantCode, type, }) => {
6
6
  if (type !== "Practitioner") {
7
7
  if (!tenantCode) {
@@ -1,11 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.appleLogin = appleLogin;
4
+ const socialLogin_1 = require("./socialLogin");
4
5
  async function appleLogin(body, requestOptions) {
5
- const { internalId, ...rest } = body;
6
- const queryParams = new URLSearchParams({
7
- client: `ClientApplication/${this.socialLoginClientId}`,
8
- internalId,
9
- });
10
- return this.post(`auth/external/apple?${queryParams.toString()}`, rest, "application/json", requestOptions);
6
+ return socialLogin_1.socialLogin.call(this, "apple", body, requestOptions);
11
7
  }
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.changePassword = changePassword;
4
+ const request_options_1 = require("../../request-options");
4
5
  async function changePassword(body, requestOptions) {
5
6
  const { oldPassword, newPassword } = body;
6
- return this.patch("v2/auth/change-password", { oldPassword, newPassword }, ...(requestOptions === undefined ? [] : [requestOptions]));
7
+ return this.patch("v2/auth/change-password", { oldPassword, newPassword }, ...(0, request_options_1.optionalArg)(requestOptions));
7
8
  }
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getSessions = getSessions;
4
+ const request_options_1 = require("../../request-options");
4
5
  /** Lists the server-side sessions for the current account. */
5
6
  async function getSessions(requestOptions) {
6
- return this.get("auth/session", ...(requestOptions === undefined ? [] : [requestOptions]));
7
+ return this.get("auth/session", ...(0, request_options_1.optionalArg)(requestOptions));
7
8
  }
@@ -1,11 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.googleLogin = googleLogin;
4
+ const socialLogin_1 = require("./socialLogin");
4
5
  async function googleLogin(body, requestOptions) {
5
- const { internalId, ...rest } = body;
6
- const queryParams = new URLSearchParams({
7
- client: `ClientApplication/${this.socialLoginClientId}`,
8
- internalId,
9
- });
10
- return this.post(`auth/external/google?${queryParams.toString()}`, rest, "application/json", requestOptions);
6
+ return socialLogin_1.socialLogin.call(this, "google", body, requestOptions);
11
7
  }
@@ -2,11 +2,20 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.startTenantLogin = startTenantLogin;
4
4
  exports.login = login;
5
- const exchange_code_1 = require("../../../utils/auth/exchange-code");
6
- const generate_code_challenge_and_verifier_1 = require("../../../utils/auth/generate-code-challenge-and-verifier");
7
- const handle_tenant_code_1 = require("../../../utils/auth/handle-tenant-code");
8
- const start_login_1 = require("../../../utils/auth/start-login");
5
+ const request_options_1 = require("../../request-options");
6
+ const exchange_code_1 = require("../exchange-code");
7
+ const generate_code_challenge_and_verifier_1 = require("../generate-code-challenge-and-verifier");
8
+ const handle_tenant_code_1 = require("../handle-tenant-code");
9
+ const start_login_1 = require("../start-login");
9
10
  const LoginError_1 = require("../types/LoginError");
11
+ /** The MFA step: verifying the code yields a session code that `exchange` turns into a session. */
12
+ const mfaStep = (client, type, loginId, exchange, requestOptions) => ({
13
+ nextStep: "mfa",
14
+ verify: async (code) => {
15
+ const { sessionCode } = await client.post(`auth/tenant/${type}/login/mfa`, { loginId, mfaToken: code }, "application/json", requestOptions);
16
+ return exchange(sessionCode);
17
+ },
18
+ });
10
19
  /** Starts practitioner sign-in and lets the app choose among returned projects. */
11
20
  async function startTenantLogin(body, requestOptions) {
12
21
  const { codeVerifier, codeChallenge } = (0, generate_code_challenge_and_verifier_1.generateCodeChallengeAndVerifier)();
@@ -23,36 +32,23 @@ async function startTenantLogin(body, requestOptions) {
23
32
  if (!tenantCode) {
24
33
  throw new LoginError_1.LoginError("tenant_code_required", "Choose a project before completing sign-in.", profiles);
25
34
  }
26
- if (startResponse.nextStep === "mfa") {
27
- return {
28
- nextStep: "mfa",
29
- verify: async (code) => {
30
- const mfaResponse = await this.post("auth/tenant/Practitioner/login/mfa", { loginId: startResponse.loginId, mfaToken: code }, "application/json", requestOptions);
31
- return (0, exchange_code_1.exchangeCode)({
32
- client: this,
33
- type: "Practitioner",
34
- sessionCode: mfaResponse.sessionCode,
35
- codeVerifier,
36
- tenantCode,
37
- requestOptions,
38
- });
39
- },
40
- };
41
- }
42
- return (0, exchange_code_1.exchangeCode)({
35
+ const exchange = (sessionCode) => (0, exchange_code_1.exchangeCode)({
43
36
  client: this,
44
37
  type: "Practitioner",
45
- sessionCode: startResponse.sessionCode,
38
+ sessionCode,
46
39
  codeVerifier,
47
40
  tenantCode,
48
41
  requestOptions,
49
42
  });
43
+ return startResponse.nextStep === "mfa"
44
+ ? mfaStep(this, "Practitioner", startResponse.loginId, exchange, requestOptions)
45
+ : exchange(startResponse.sessionCode);
50
46
  },
51
47
  };
52
48
  }
53
49
  async function login(body, requestOptions) {
54
50
  if (body.type === "Practitioner") {
55
- const start = await startTenantLogin.call(this, body, ...(requestOptions === undefined ? [] : [requestOptions]));
51
+ const start = await startTenantLogin.call(this, body, ...(0, request_options_1.optionalArg)(requestOptions));
56
52
  return start.exchange(body.tenantCode);
57
53
  }
58
54
  const { codeVerifier, codeChallenge } = (0, generate_code_challenge_and_verifier_1.generateCodeChallengeAndVerifier)();
@@ -62,30 +58,16 @@ async function login(body, requestOptions) {
62
58
  codeChallenge,
63
59
  requestOptions,
64
60
  });
65
- if (startResponse.nextStep === "mfa") {
66
- return {
67
- nextStep: "mfa",
68
- verify: async (code) => {
69
- const mfaResponse = await this.post(`auth/tenant/${body.type}/login/mfa`, {
70
- loginId: startResponse.loginId,
71
- mfaToken: code,
72
- }, "application/json", requestOptions);
73
- return (0, exchange_code_1.exchangeCode)({
74
- client: this,
75
- type: "Patient",
76
- sessionCode: mfaResponse.sessionCode,
77
- codeVerifier,
78
- requestOptions,
79
- });
80
- },
81
- };
82
- }
83
- (0, handle_tenant_code_1.handleTenantCode)({ profiles: startResponse.profiles, tenantCode: body.tenantCode, type: "Patient" });
84
- return (0, exchange_code_1.exchangeCode)({
61
+ const exchange = (sessionCode) => (0, exchange_code_1.exchangeCode)({
85
62
  client: this,
86
63
  type: "Patient",
87
- sessionCode: startResponse.sessionCode,
64
+ sessionCode,
88
65
  codeVerifier,
89
66
  requestOptions,
90
67
  });
68
+ if (startResponse.nextStep === "mfa") {
69
+ return mfaStep(this, body.type, startResponse.loginId, exchange, requestOptions);
70
+ }
71
+ (0, handle_tenant_code_1.handleTenantCode)({ profiles: startResponse.profiles, tenantCode: body.tenantCode, type: "Patient" });
72
+ return exchange(startResponse.sessionCode);
91
73
  }
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.logout = logout;
4
+ const request_options_1 = require("../../request-options");
4
5
  const account_storage_1 = require("../account-storage");
5
6
  /** Revokes the current server session and always removes its local account. */
6
7
  async function logout(requestOptions) {
7
8
  var _a, _b;
8
9
  const accountId = (_b = (_a = this.getActiveLogin()) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
9
10
  try {
10
- await this.delete("auth/session/current", ...(requestOptions === undefined ? [] : [requestOptions]));
11
+ await this.delete("auth/session/current", ...(0, request_options_1.optionalArg)(requestOptions));
11
12
  }
12
13
  finally {
13
14
  this.clearActiveLogin();
@@ -9,8 +9,9 @@ exports.requestDeleteUser = requestDeleteUser;
9
9
  * @returns {Promise<RequestDeleteUserResponse>} Response containing scheduled deletion status and user information
10
10
  */
11
11
  async function requestDeleteUser(body, requestOptions) {
12
- return this.delete("auth/delete", { ...{
13
- body: JSON.stringify(body),
14
- headers: { "Content-Type": "application/json" },
15
- }, ...requestOptions });
12
+ return this.delete("auth/delete", {
13
+ body: JSON.stringify(body),
14
+ headers: { "Content-Type": "application/json" },
15
+ ...requestOptions,
16
+ });
16
17
  }
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.revokeSessions = revokeSessions;
4
+ const request_options_1 = require("../../request-options");
4
5
  /** Revokes the selected server-side session or session group. */
5
6
  async function revokeSessions(option, requestOptions) {
6
- await this.delete(`auth/session/${encodeURIComponent(option)}`, ...(requestOptions === undefined ? [] : [requestOptions]));
7
+ await this.delete(`auth/session/${encodeURIComponent(option)}`, ...(0, request_options_1.optionalArg)(requestOptions));
7
8
  if (option === "current") {
8
9
  this.clearActiveLogin();
9
10
  }
@@ -0,0 +1,7 @@
1
+ import type { MedplumRequestOptions } from "@medplum/core";
2
+ import { OvokClient } from "../../ovok-client";
3
+ import { AppleLoginBody } from "../types/AppleLoginBody";
4
+ import { AuthResponse } from "../types/AuthResponse";
5
+ import { GoogleLoginBody } from "../types/GoogleLoginBody";
6
+ /** Signs in with a provider token through the social-login client. Not bound onto OvokClient itself. */
7
+ export declare function socialLogin(this: OvokClient, provider: "apple" | "google", { internalId, ...rest }: AppleLoginBody | GoogleLoginBody, requestOptions?: MedplumRequestOptions): Promise<AuthResponse>;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.socialLogin = socialLogin;
4
+ /** Signs in with a provider token through the social-login client. Not bound onto OvokClient itself. */
5
+ function socialLogin(provider, { internalId, ...rest }, requestOptions) {
6
+ const queryParams = new URLSearchParams({
7
+ client: `ClientApplication/${this.socialLoginClientId}`,
8
+ internalId,
9
+ });
10
+ return this.post(`auth/external/${provider}?${queryParams.toString()}`, rest, "application/json", requestOptions);
11
+ }
@@ -0,0 +1,35 @@
1
+ import { IClientStorage } from "@medplum/core";
2
+ import type { OvokClientOptions } from "../ovok-client";
3
+ export type OvokUnauthenticatedReason = {
4
+ type: "refresh-rejected";
5
+ status: 400 | 401 | 403;
6
+ } | {
7
+ type: "refresh-unavailable";
8
+ status?: number;
9
+ } | {
10
+ type: "authentication-rejected";
11
+ status: number;
12
+ } | {
13
+ type: "request-unauthorized";
14
+ status: 401;
15
+ };
16
+ type SessionFetchContext = {
17
+ config: OvokClientOptions;
18
+ storage: IClientStorage;
19
+ fetchImplementation: (url: string, options?: RequestInit) => Promise<Response>;
20
+ normalizedBaseUrl: string;
21
+ tokenUrl: string;
22
+ };
23
+ /**
24
+ * The client's `fetch`: request timeouts, token-refresh retries within the access token's
25
+ * lifetime, and tracking why a session ended, which `onUnauthenticated` reports.
26
+ */
27
+ export declare function createSessionFetch({ config, storage, fetchImplementation, normalizedBaseUrl, tokenUrl }: SessionFetchContext): {
28
+ fetch: (url: string, options: RequestInit | undefined) => Promise<Response>;
29
+ onUnauthenticated: () => void;
30
+ refreshState: {
31
+ unavailable: boolean;
32
+ };
33
+ setClearActiveLogin: (clear: () => void) => void;
34
+ };
35
+ export {};
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSessionFetch = createSessionFetch;
4
+ /* eslint-disable max-lines -- one refresh state machine; splitting it would scatter its shared session state. */
5
+ const core_1 = require("@medplum/core");
6
+ const account_storage_1 = require("./account-storage");
7
+ const session_http_1 = require("./session-http");
8
+ /**
9
+ * The client's `fetch`: request timeouts, token-refresh retries within the access token's
10
+ * lifetime, and tracking why a session ended, which `onUnauthenticated` reports.
11
+ */
12
+ function createSessionFetch({ config, storage, fetchImplementation, normalizedBaseUrl, tokenUrl }) {
13
+ let unauthenticatedReason;
14
+ let clearActiveLogin = () => undefined;
15
+ let terminalRefreshToken;
16
+ let terminalRefreshError;
17
+ const refreshState = { unavailable: false };
18
+ const isTokenRefresh = (url, options) => new URL(url, normalizedBaseUrl).href === tokenUrl &&
19
+ (options === null || options === void 0 ? void 0 : options.method) === "POST" &&
20
+ typeof options.body === "string" &&
21
+ new URLSearchParams(options.body).get("grant_type") === "refresh_token";
22
+ const activeAccountId = () => { var _a, _b; return (_b = (_a = storage.getObject(account_storage_1.ACTIVE_LOGIN_STORAGE_KEY)) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference; };
23
+ const markActiveAccountExpired = () => {
24
+ const accountId = activeAccountId();
25
+ if (accountId) {
26
+ (0, account_storage_1.setStoredAccountExpired)(storage, accountId, true);
27
+ }
28
+ };
29
+ const fetchOnce = async (url, options) => {
30
+ const response = await (0, session_http_1.fetchWithTimeout)(fetchImplementation, url, options, config.requestTimeoutMs);
31
+ const requestUrl = new URL(url, normalizedBaseUrl);
32
+ if (isTokenRefresh(url, options)) {
33
+ if (response.status === 400) {
34
+ let invalidGrant = false;
35
+ try {
36
+ invalidGrant = (await response.clone().json()).error === "invalid_grant";
37
+ }
38
+ catch (_a) {
39
+ // Do not sign out unless the response confirms that the refresh token was revoked.
40
+ }
41
+ if (!invalidGrant) {
42
+ throw (0, session_http_1.refreshResponseError)(response);
43
+ }
44
+ }
45
+ if ([400, 401, 403].includes(response.status)) {
46
+ unauthenticatedReason = {
47
+ type: "refresh-rejected",
48
+ status: response.status,
49
+ };
50
+ markActiveAccountExpired();
51
+ }
52
+ else if (response.ok) {
53
+ const accountId = activeAccountId();
54
+ if (accountId) {
55
+ (0, account_storage_1.setStoredAccountExpired)(storage, accountId, false);
56
+ }
57
+ unauthenticatedReason = undefined;
58
+ }
59
+ }
60
+ else if (requestUrl.href === tokenUrl && !response.ok) {
61
+ unauthenticatedReason = {
62
+ type: "authentication-rejected",
63
+ status: response.status,
64
+ };
65
+ }
66
+ else if (response.status === 401) {
67
+ unauthenticatedReason = { type: "request-unauthorized", status: 401 };
68
+ }
69
+ return isTokenRefresh(url, options)
70
+ ? response
71
+ : (0, session_http_1.withHttpErrorMetadata)(response);
72
+ };
73
+ const fetchWithRefreshRetry = async (url, options) => {
74
+ var _a, _b, _c, _d;
75
+ const requestUrl = new URL(url, normalizedBaseUrl);
76
+ if (!isTokenRefresh(url, options)) {
77
+ const response = await fetchOnce(url, options);
78
+ const sessionEnded = requestUrl.pathname.endsWith("/auth/session/current") && response.status === 401;
79
+ if (sessionEnded) {
80
+ markActiveAccountExpired();
81
+ }
82
+ return response;
83
+ }
84
+ const login = storage.getObject(account_storage_1.ACTIVE_LOGIN_STORAGE_KEY);
85
+ if (terminalRefreshError) {
86
+ if ((login === null || login === void 0 ? void 0 : login.accessToken) && login.accessToken !== terminalRefreshToken) {
87
+ terminalRefreshToken = undefined;
88
+ terminalRefreshError = undefined;
89
+ }
90
+ else {
91
+ throw terminalRefreshError;
92
+ }
93
+ }
94
+ const expiresAt = (login === null || login === void 0 ? void 0 : login.accessToken) ? (0, core_1.tryGetJwtExpiration)(login.accessToken) : undefined;
95
+ const signal = options === null || options === void 0 ? void 0 : options.signal;
96
+ if (expiresAt !== undefined && options) {
97
+ const tokenRequestOptions = options;
98
+ tokenRequestOptions.maxRetries = 0;
99
+ tokenRequestOptions.maxRetryTime = 0;
100
+ }
101
+ if (expiresAt === undefined) {
102
+ const response = await fetchOnce(url, options);
103
+ if ((0, session_http_1.isTemporaryRefreshFailure)(response.status)) {
104
+ void ((_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel().catch(() => undefined));
105
+ if (config.keepSessionOnRefreshUnavailable) {
106
+ refreshState.unavailable = true;
107
+ }
108
+ throw (0, session_http_1.refreshUnavailableError)(response);
109
+ }
110
+ return response;
111
+ }
112
+ let attempt = 0;
113
+ let firstAttempt = true;
114
+ let lastResponse;
115
+ let lastNetworkError;
116
+ while (firstAttempt || Date.now() < expiresAt) {
117
+ firstAttempt = false;
118
+ try {
119
+ lastResponse = await fetchOnce(url, options);
120
+ }
121
+ catch (error) {
122
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) || !(0, session_http_1.isNetworkError)(error)) {
123
+ throw error;
124
+ }
125
+ if (config.keepSessionOnRefreshUnavailable) {
126
+ refreshState.unavailable = true;
127
+ lastNetworkError = error;
128
+ break;
129
+ }
130
+ if (Date.now() >= expiresAt) {
131
+ lastNetworkError = error;
132
+ break;
133
+ }
134
+ lastNetworkError = error;
135
+ const remainingMs = expiresAt - Date.now();
136
+ const backoffMs = (0, session_http_1.refreshBackoffMs)(attempt);
137
+ await (0, session_http_1.waitForRetry)(Math.min(backoffMs, remainingMs), signal);
138
+ attempt += 1;
139
+ continue;
140
+ }
141
+ if (!(0, session_http_1.isTemporaryRefreshFailure)(lastResponse.status)) {
142
+ return lastResponse;
143
+ }
144
+ if (config.keepSessionOnRefreshUnavailable) {
145
+ refreshState.unavailable = true;
146
+ break;
147
+ }
148
+ const remainingMs = expiresAt - Date.now();
149
+ if (remainingMs <= 0) {
150
+ break;
151
+ }
152
+ const backoffMs = (0, session_http_1.refreshBackoffMs)(attempt);
153
+ const waitMs = Math.min(Math.max((_b = (0, session_http_1.retryAfterMs)(lastResponse)) !== null && _b !== void 0 ? _b : 0, backoffMs), remainingMs);
154
+ if (waitMs >= remainingMs) {
155
+ await (0, session_http_1.waitForRetry)(remainingMs, signal);
156
+ break;
157
+ }
158
+ void ((_c = lastResponse.body) === null || _c === void 0 ? void 0 : _c.cancel().catch(() => undefined));
159
+ await (0, session_http_1.waitForRetry)(waitMs, signal);
160
+ attempt += 1;
161
+ }
162
+ const status = lastResponse === null || lastResponse === void 0 ? void 0 : lastResponse.status;
163
+ const reason = {
164
+ type: "refresh-unavailable",
165
+ ...(status === undefined ? {} : { status }),
166
+ };
167
+ if (config.keepSessionOnRefreshUnavailable) {
168
+ refreshState.unavailable = true;
169
+ if (lastResponse) {
170
+ return lastResponse;
171
+ }
172
+ throw lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : (0, session_http_1.refreshUnavailableError)(undefined);
173
+ }
174
+ unauthenticatedReason = reason;
175
+ markActiveAccountExpired();
176
+ if (lastResponse) {
177
+ return lastResponse;
178
+ }
179
+ terminalRefreshToken = login === null || login === void 0 ? void 0 : login.accessToken;
180
+ terminalRefreshError = lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : (0, session_http_1.refreshUnavailableError)(undefined);
181
+ clearActiveLogin();
182
+ (_d = config.onUnauthenticated) === null || _d === void 0 ? void 0 : _d.call(config, reason);
183
+ unauthenticatedReason = undefined;
184
+ throw terminalRefreshError;
185
+ };
186
+ const onUnauthenticated = () => {
187
+ var _a;
188
+ if (refreshState.unavailable) {
189
+ return;
190
+ }
191
+ const reason = unauthenticatedReason !== null && unauthenticatedReason !== void 0 ? unauthenticatedReason : {
192
+ type: "request-unauthorized",
193
+ status: 401,
194
+ };
195
+ unauthenticatedReason = undefined;
196
+ (_a = config.onUnauthenticated) === null || _a === void 0 ? void 0 : _a.call(config, reason);
197
+ };
198
+ return {
199
+ fetch: fetchWithRefreshRetry,
200
+ onUnauthenticated,
201
+ refreshState,
202
+ setClearActiveLogin: (clear) => {
203
+ clearActiveLogin = clear;
204
+ },
205
+ };
206
+ }
@@ -0,0 +1,11 @@
1
+ export declare const retryAfterMs: (response: Response) => number | undefined;
2
+ export declare const withHttpErrorMetadata: (response: Response) => Promise<Response>;
3
+ export declare const waitForRetry: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
4
+ export declare const isTemporaryRefreshFailure: (status: number) => boolean;
5
+ export declare const refreshUnavailableError: (response?: Response) => Error;
6
+ export declare const refreshResponseError: (response: Response) => Error;
7
+ export declare const isNetworkError: (error: unknown) => boolean;
8
+ /** Fetches with an upper time bound, still honouring the caller's own abort signal. */
9
+ export declare const fetchWithTimeout: (fetchImplementation: (url: string, options?: RequestInit) => Promise<Response>, url: string, options: RequestInit | undefined, requestTimeoutMs: number | undefined) => Promise<Response>;
10
+ /** Exponential refresh backoff: 500 ms doubling per attempt, capped at 30 s. */
11
+ export declare const refreshBackoffMs: (attempt: number) => number;