@ovok/core 0.4.36 → 0.4.37

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 (52) hide show
  1. package/dist/client/auth/session-fetch.d.ts +35 -0
  2. package/dist/client/auth/session-fetch.js +207 -0
  3. package/dist/client/auth/session-http.d.ts +11 -0
  4. package/dist/client/auth/session-http.js +180 -0
  5. package/dist/client/experience/methods.js +27 -37
  6. package/dist/client/notifications/methods.js +4 -5
  7. package/dist/client/observation/services/creator.js +4 -18
  8. package/dist/client/observation/services/single-value-services.d.ts +31 -0
  9. package/dist/client/observation/services/single-value-services.js +95 -0
  10. package/dist/client/ovok-client.d.ts +2 -13
  11. package/dist/client/ovok-client.js +8 -356
  12. package/dist/client/patient/methods.js +2 -11
  13. package/dist/client/patient-sharing/methods.js +1 -2
  14. package/dist/client/platform/methods.js +14 -24
  15. package/dist/client/query-string.d.ts +3 -0
  16. package/dist/client/query-string.js +15 -0
  17. package/dist/client/questionnaire-response/methods/extractQuestionnaireObservations.js +1 -2
  18. package/dist/client/questionnaire-response/methods/populateQuestionnaire.js +1 -2
  19. package/dist/client/signals/methods.js +16 -17
  20. package/dist/client/slim/methods.js +58 -68
  21. package/dist/conformance/capability-requirements.js +1 -1
  22. package/dist/hooks/account-hooks.js +6 -68
  23. package/dist/hooks/api-hooks.d.ts +5 -0
  24. package/dist/hooks/api-hooks.js +22 -67
  25. package/dist/hooks/email-hooks.js +1 -40
  26. package/dist/hooks/experience-hooks.js +4 -60
  27. package/dist/hooks/notification-hooks.js +5 -55
  28. package/dist/hooks/notification-invalidation.d.ts +6 -0
  29. package/dist/hooks/notification-invalidation.js +52 -0
  30. package/dist/hooks/patient-hooks.js +13 -113
  31. package/dist/hooks/patient-sharing-hooks.js +1 -5
  32. package/dist/hooks/platform-hooks.js +3 -51
  33. package/dist/hooks/signals-hooks.js +2 -45
  34. package/package.json +1 -1
  35. package/dist/client/observation/services/blood-glucose.d.ts +0 -12
  36. package/dist/client/observation/services/blood-glucose.js +0 -36
  37. package/dist/client/observation/services/body-temperature.d.ts +0 -13
  38. package/dist/client/observation/services/body-temperature.js +0 -36
  39. package/dist/client/observation/services/body-weight.d.ts +0 -13
  40. package/dist/client/observation/services/body-weight.js +0 -36
  41. package/dist/client/observation/services/heart-rate-variability.d.ts +0 -13
  42. package/dist/client/observation/services/heart-rate-variability.js +0 -38
  43. package/dist/client/observation/services/heart-rate.d.ts +0 -13
  44. package/dist/client/observation/services/heart-rate.js +0 -39
  45. package/dist/client/observation/services/respiratory-rate.d.ts +0 -13
  46. package/dist/client/observation/services/respiratory-rate.js +0 -38
  47. package/dist/client/observation/services/resting-heart-rate.d.ts +0 -13
  48. package/dist/client/observation/services/resting-heart-rate.js +0 -38
  49. package/dist/client/observation/services/vo2-max.d.ts +0 -13
  50. package/dist/client/observation/services/vo2-max.js +0 -38
  51. package/dist/client/observation/services/walking-heart-rate-average.d.ts +0 -12
  52. package/dist/client/observation/services/walking-heart-rate-average.js +0 -38
@@ -0,0 +1,31 @@
1
+ import { MeasurementTypeKey } from "../../../types";
2
+ import { RecordResult, RecordType } from "../types/generate-measurement/HealthConnectTypes";
3
+ import { Sample } from "../types/generate-measurement/Sample";
4
+ import { Measurement } from "../types/measurement/Measurement";
5
+ import { ObservationMetadata } from "../types/service/ObservationMetadata";
6
+ /** A measurement whose one value sits under the spec's `valueKey`. */
7
+ type SingleValueMeasurement = Measurement & {
8
+ [value: string]: unknown;
9
+ };
10
+ import { BaseObservationService } from "./base-observation-service";
11
+ type SingleValueSpec = {
12
+ measurementTypeKey: MeasurementTypeKey;
13
+ measurementTypeName: string;
14
+ metadata: Omit<ObservationMetadata<SingleValueMeasurement>, "additionalCodes">;
15
+ /** HealthKit quantity type to read; the first sample is used when omitted. */
16
+ sampleType?: string;
17
+ fromRecord: (record: RecordResult<RecordType>) => number | undefined;
18
+ };
19
+ /** An observation with one value: what varies per measurement is data, not code. */
20
+ declare class SingleValueObservationService extends BaseObservationService<SingleValueMeasurement> {
21
+ private readonly spec;
22
+ protected measurementTypeKey: MeasurementTypeKey;
23
+ protected measurementTypeName: string;
24
+ protected observationMetadata: ObservationMetadata<SingleValueMeasurement>[];
25
+ constructor(spec: SingleValueSpec);
26
+ convertSamplesToMeasurement(samples: Sample[]): SingleValueMeasurement;
27
+ convertRecordToMeasurement(record: RecordResult<RecordType>): SingleValueMeasurement;
28
+ private measurement;
29
+ }
30
+ export declare const singleValueObservationServices: () => [MeasurementTypeKey, SingleValueObservationService][];
31
+ export {};
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.singleValueObservationServices = void 0;
4
+ const types_1 = require("../../../types");
5
+ const base_observation_service_1 = require("./base-observation-service");
6
+ /** An observation with one value: what varies per measurement is data, not code. */
7
+ class SingleValueObservationService extends base_observation_service_1.BaseObservationService {
8
+ constructor(spec) {
9
+ super();
10
+ this.spec = spec;
11
+ this.measurementTypeKey = spec.measurementTypeKey;
12
+ this.measurementTypeName = spec.measurementTypeName;
13
+ this.observationMetadata = [{ ...spec.metadata, additionalCodes: [] }];
14
+ }
15
+ convertSamplesToMeasurement(samples) {
16
+ const sample = this.spec.sampleType
17
+ ? samples.find((x) => x.quantityType === this.spec.sampleType)
18
+ : samples[0];
19
+ return this.measurement(sample === null || sample === void 0 ? void 0 : sample.quantity);
20
+ }
21
+ convertRecordToMeasurement(record) {
22
+ return this.measurement(this.spec.fromRecord(record));
23
+ }
24
+ measurement(value) {
25
+ return {
26
+ [this.spec.metadata.valueKey]: value,
27
+ measurementTypeKey: this.measurementTypeKey,
28
+ };
29
+ }
30
+ }
31
+ const specs = [
32
+ {
33
+ measurementTypeKey: types_1.MeasurementTypeKey.bloodGlucose,
34
+ measurementTypeName: "blood glucose",
35
+ metadata: { primaryCode: types_1.ObservationCode.BLOOD_GLUCOSE, display: "Blood Glucose", unit: "mg/dL", valueKey: "bloodGlucose", toFixed: 1 },
36
+ fromRecord: (record) => record.level.inMilligramsPerDeciliter,
37
+ },
38
+ {
39
+ measurementTypeKey: types_1.MeasurementTypeKey.temperature,
40
+ measurementTypeName: "body temperature",
41
+ metadata: { primaryCode: types_1.ObservationCode.BODY_TEMPERATURE, display: "Body Temperature", unit: "°C", valueKey: "bodyTemp", toFixed: 2 },
42
+ fromRecord: (record) => record.temperature.inCelsius,
43
+ },
44
+ {
45
+ measurementTypeKey: types_1.MeasurementTypeKey.bodyWeight,
46
+ measurementTypeName: "body weight",
47
+ metadata: { primaryCode: types_1.ObservationCode.BODY_WEIGHT, display: "Body Weight", unit: "kg", valueKey: "bodyWeight", toFixed: 2 },
48
+ fromRecord: (record) => record.weight.inKilograms,
49
+ },
50
+ {
51
+ measurementTypeKey: types_1.MeasurementTypeKey.heartRateVariability,
52
+ measurementTypeName: "heart rate variability",
53
+ metadata: { primaryCode: types_1.ObservationCode.HEART_RATE_VARIABILITY, display: "Heart Rate Variability", unit: "ms", valueKey: "heartRateVariability", toFixed: 1 },
54
+ sampleType: "HKQuantityTypeIdentifierHeartRateVariabilitySDNN",
55
+ fromRecord: (record) => record.heartRateVariabilityMillis,
56
+ },
57
+ {
58
+ measurementTypeKey: types_1.MeasurementTypeKey.heartRate,
59
+ measurementTypeName: "heart rate",
60
+ metadata: { primaryCode: types_1.ObservationCode.HEART_RATE, display: "Heart Rate", unit: "bpm", valueKey: "heartRate", toFixed: 0 },
61
+ sampleType: "HKQuantityTypeIdentifierHeartRate",
62
+ fromRecord: (record) => { var _a, _b; return (_b = (_a = record.samples) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.beatsPerMinute; },
63
+ },
64
+ {
65
+ measurementTypeKey: types_1.MeasurementTypeKey.respiratoryRate,
66
+ measurementTypeName: "respiratory rate",
67
+ metadata: { primaryCode: types_1.ObservationCode.RESPIRATORY_RATE, display: "Respiratory Rate", unit: "breaths/min", valueKey: "respiratoryRate", toFixed: 1 },
68
+ sampleType: "HKQuantityTypeIdentifierRespiratoryRate",
69
+ fromRecord: (record) => record.rate,
70
+ },
71
+ {
72
+ measurementTypeKey: types_1.MeasurementTypeKey.restingHeartRate,
73
+ measurementTypeName: "resting heart rate",
74
+ metadata: { primaryCode: types_1.ObservationCode.RESTING_HEART_RATE, display: "Resting Heart Rate", unit: "bpm", valueKey: "restingHeartRate", toFixed: 0 },
75
+ sampleType: "HKQuantityTypeIdentifierRestingHeartRate",
76
+ fromRecord: (record) => record.beatsPerMinute,
77
+ },
78
+ {
79
+ measurementTypeKey: types_1.MeasurementTypeKey.vo2Max,
80
+ measurementTypeName: "VO2 max",
81
+ metadata: { primaryCode: types_1.ObservationCode.VO2_MAX, display: "VO2 Max", unit: "mL/kg/min", valueKey: "vo2Max", toFixed: 1 },
82
+ sampleType: "HKQuantityTypeIdentifierVO2Max",
83
+ fromRecord: (record) => record.vo2MillilitersPerMinuteKilogram,
84
+ },
85
+ {
86
+ measurementTypeKey: types_1.MeasurementTypeKey.walkingHeartRateAverage,
87
+ measurementTypeName: "walking heart rate average",
88
+ metadata: { primaryCode: types_1.ObservationCode.WALKING_HEART_RATE_AVERAGE, display: "Walking Heart Rate Average", unit: "bpm", valueKey: "walkingHeartRateAverage", toFixed: 0 },
89
+ sampleType: "HKQuantityTypeIdentifierWalkingHeartRateAverage",
90
+ // Health Connect has no walking-heart-rate-average record.
91
+ fromRecord: () => 0,
92
+ },
93
+ ];
94
+ const singleValueObservationServices = () => specs.map((spec) => [spec.measurementTypeKey, new SingleValueObservationService(spec)]);
95
+ exports.singleValueObservationServices = singleValueObservationServices;
@@ -3,6 +3,7 @@ import { Bundle, CapabilityStatement, Observation } from "@medplum/fhirtypes";
3
3
  import { OvokPublicApiEndpoint } from "../conformance/capability";
4
4
  import * as aiFhirMethods from "./ai-fhir/methods";
5
5
  import * as authMethods from "./auth/methods";
6
+ import { OvokUnauthenticatedReason } from "./auth/session-fetch";
6
7
  import { OvokSavedAccount } from "./auth/types/Account";
7
8
  import * as botMethods from "./bot/methods";
8
9
  import * as changeHistoryMethods from "./change-history/methods";
@@ -24,6 +25,7 @@ import * as signalsMethods from "./signals/methods";
24
25
  import * as slimMethods from "./slim/methods";
25
26
  export declare const MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
26
27
  export { MAX_OBSERVATION_BUNDLE_BYTES } from "./observation/observation-bundle-utils";
28
+ export type { OvokUnauthenticatedReason } from "./auth/session-fetch";
27
29
  export type SaveObservationsResult = {
28
30
  status: "saved";
29
31
  response: Bundle;
@@ -32,19 +34,6 @@ export type SaveObservationsResult = {
32
34
  queueId: string;
33
35
  };
34
36
  export type OvokFhirVersion = "R4" | "R5";
35
- export type OvokUnauthenticatedReason = {
36
- type: "refresh-rejected";
37
- status: 400 | 401 | 403;
38
- } | {
39
- type: "refresh-unavailable";
40
- status?: number;
41
- } | {
42
- type: "authentication-rejected";
43
- status: number;
44
- } | {
45
- type: "request-unauthorized";
46
- status: 401;
47
- };
48
37
  export type OvokClientOptions = Omit<MedplumClientOptions, "onUnauthenticated"> & {
49
38
  onUnauthenticated?: (reason: OvokUnauthenticatedReason) => void;
50
39
  socialLoginClientId?: string;
@@ -41,6 +41,8 @@ const aiFhirMethods = __importStar(require("./ai-fhir/methods"));
41
41
  const account_storage_1 = require("./auth/account-storage");
42
42
  const errors_1 = require("./auth/errors");
43
43
  const authMethods = __importStar(require("./auth/methods"));
44
+ const session_fetch_1 = require("./auth/session-fetch");
45
+ const session_http_1 = require("./auth/session-http");
44
46
  const botMethods = __importStar(require("./bot/methods"));
45
47
  const changeHistoryMethods = __importStar(require("./change-history/methods"));
46
48
  const aiMethods = __importStar(require("./chat/ai/methods"));
@@ -48,7 +50,6 @@ const cmsMethods = __importStar(require("./cms/methods"));
48
50
  const consentMethods = __importStar(require("./consent/methods"));
49
51
  const emailMethods = __importStar(require("./email"));
50
52
  const ovok_error_1 = require("./errors/ovok-error");
51
- const ovok_error_utils_1 = require("./errors/ovok-error-utils");
52
53
  const rate_limit_error_1 = require("./errors/rate-limit-error");
53
54
  const experienceMethods = __importStar(require("./experience/methods"));
54
55
  const inviteMethods = __importStar(require("./invites/methods"));
@@ -66,138 +67,6 @@ const slimMethods = __importStar(require("./slim/methods"));
66
67
  exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
67
68
  var observation_bundle_utils_2 = require("./observation/observation-bundle-utils");
68
69
  Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
69
- const retryAfterMs = (response) => {
70
- var _a, _b;
71
- const value = (_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, "Retry-After");
72
- if (!value) {
73
- return undefined;
74
- }
75
- const seconds = Number(value);
76
- if (Number.isFinite(seconds)) {
77
- return Math.max(0, seconds * 1000);
78
- }
79
- const date = Date.parse(value);
80
- return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
81
- };
82
- const timeoutError = () => {
83
- const error = new Error("The request timed out.");
84
- error.name = "TimeoutError";
85
- return error;
86
- };
87
- const withHttpErrorMetadata = async (response) => {
88
- var _a, _b;
89
- if (response.ok || typeof response.clone !== "function") {
90
- return response;
91
- }
92
- let bodyText = "";
93
- let body;
94
- try {
95
- bodyText = await response.clone().text();
96
- body = JSON.parse(bodyText);
97
- }
98
- catch (_c) {
99
- body = undefined;
100
- }
101
- const outcome = body && typeof body === "object" && body.resourceType === "OperationOutcome"
102
- ? body
103
- : undefined;
104
- const envelope = body && typeof body === "object" ? body : undefined;
105
- const nestedError = (envelope === null || envelope === void 0 ? void 0 : envelope.error) && typeof envelope.error === "object"
106
- ? envelope.error
107
- : undefined;
108
- const safeResponse = envelope
109
- ? Object.fromEntries(Object.entries(envelope).filter(([key]) => ["code", "error", "message", "requestId", "retryAfterSec", "status", "statusCode", "errors", "missing"].includes(key)))
110
- : { message: typeof body === "string" ? body : response.statusText };
111
- const status = response.status;
112
- const retryAfter = (_a = retryAfterMs(response)) !== null && _a !== void 0 ? _a : (typeof (envelope === null || envelope === void 0 ? void 0 : envelope.retryAfterSec) === "number" ? envelope.retryAfterSec * 1000 : undefined);
113
- const requestId = (_b = response.headers.get("x-request-id")) !== null && _b !== void 0 ? _b : (typeof (envelope === null || envelope === void 0 ? void 0 : envelope.requestId) === "string" ? envelope.requestId : undefined);
114
- const message = typeof (envelope === null || envelope === void 0 ? void 0 : envelope.message) === "string"
115
- ? envelope.message
116
- : typeof (nestedError === null || nestedError === void 0 ? void 0 : nestedError.message) === "string"
117
- ? nestedError.message
118
- : response.statusText || `HTTP ${status}`;
119
- const extensions = [];
120
- const addMetadata = (url, value, kind) => {
121
- if (value !== undefined) {
122
- extensions.push({ url, [`value${kind}`]: value });
123
- }
124
- };
125
- addMetadata("status", status, "Integer");
126
- addMetadata("requestId", requestId, "String");
127
- addMetadata("retryAfterMs", retryAfter, "Integer");
128
- if (!outcome) {
129
- extensions.push({ url: "response", valueString: JSON.stringify(safeResponse) });
130
- }
131
- const enrichedOutcome = outcome !== null && outcome !== void 0 ? outcome : {
132
- resourceType: "OperationOutcome",
133
- id: "ovok-http-error",
134
- issue: [{
135
- severity: "error",
136
- code: status === 429 ? "throttled" : "exception",
137
- details: { text: bodyText || message },
138
- }],
139
- };
140
- const existingExtensions = Array.isArray(enrichedOutcome.extension)
141
- ? enrichedOutcome.extension
142
- : [];
143
- enrichedOutcome.extension = [
144
- ...existingExtensions,
145
- { url: ovok_error_utils_1.OVOK_HTTP_ERROR_EXTENSION_URL, extension: extensions },
146
- ];
147
- const headers = new Headers(response.headers);
148
- headers.delete("content-length");
149
- headers.set("content-type", "application/fhir+json");
150
- return new Response(JSON.stringify(enrichedOutcome), {
151
- status,
152
- statusText: response.statusText,
153
- headers,
154
- });
155
- };
156
- const waitForRetry = (milliseconds, signal) => new Promise((resolve, reject) => {
157
- var _a;
158
- if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
159
- const error = new Error("Token refresh aborted");
160
- error.name = "AbortError";
161
- reject((_a = signal.reason) !== null && _a !== void 0 ? _a : error);
162
- return;
163
- }
164
- const timer = setTimeout(() => {
165
- signal === null || signal === void 0 ? void 0 : signal.removeEventListener("abort", abort);
166
- resolve();
167
- }, milliseconds);
168
- const abort = () => {
169
- var _a;
170
- clearTimeout(timer);
171
- const error = new Error("Token refresh aborted");
172
- error.name = "AbortError";
173
- reject((_a = signal === null || signal === void 0 ? void 0 : signal.reason) !== null && _a !== void 0 ? _a : error);
174
- };
175
- signal === null || signal === void 0 ? void 0 : signal.addEventListener("abort", abort, { once: true });
176
- });
177
- const isTemporaryRefreshFailure = (status) => status === 429 || (status >= 500 && status <= 599);
178
- const refreshUnavailableError = (response) => {
179
- const error = new Error("Token refresh is temporarily unavailable");
180
- error.name = "NetworkError";
181
- Object.assign(error, { cause: response });
182
- return error;
183
- };
184
- const refreshResponseError = (response) => {
185
- const error = new Error(`Token refresh returned HTTP ${response.status}`);
186
- error.name = "TokenRefreshError";
187
- Object.assign(error, { status: response.status, cause: response });
188
- return error;
189
- };
190
- const isNetworkError = (error) => {
191
- if (!(error instanceof Error)) {
192
- return false;
193
- }
194
- if (error.name === "NetworkError" ||
195
- error.name === "UnknownHostException" ||
196
- /network request failed|failed to fetch|load failed|unknownhostexception|econn|enotfound/i.test(error.message)) {
197
- return true;
198
- }
199
- return isNetworkError(error.cause);
200
- };
201
70
  const isRetrySafePost = (pathname) => pathname === "/ai/fhir/search";
202
71
  class OvokClient extends core_1.MedplumClient {
203
72
  /**
@@ -214,236 +83,19 @@ class OvokClient extends core_1.MedplumClient {
214
83
  const baseUrl = (_c = config.baseUrl) !== null && _c !== void 0 ? _c : "https://api.medplum.com/";
215
84
  const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
216
85
  const tokenUrl = new URL((_d = config.tokenUrl) !== null && _d !== void 0 ? _d : "oauth2/token", normalizedBaseUrl).href;
217
- let unauthenticatedReason;
218
- let clearActiveLogin = () => undefined;
219
- let terminalRefreshToken;
220
- let terminalRefreshError;
221
- const refreshState = { unavailable: false };
222
- const isTokenRefresh = (url, options) => new URL(url, normalizedBaseUrl).href === tokenUrl &&
223
- (options === null || options === void 0 ? void 0 : options.method) === "POST" &&
224
- typeof options.body === "string" &&
225
- new URLSearchParams(options.body).get("grant_type") === "refresh_token";
226
- const markActiveAccountExpired = () => {
227
- var _a, _b;
228
- const accountId = (_b = (_a = storage.getObject("activeLogin")) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
229
- if (accountId) {
230
- (0, account_storage_1.setStoredAccountExpired)(storage, accountId, true);
231
- }
232
- };
233
- const fetchOnce = async (url, options) => {
234
- var _a, _b;
235
- const requestTimeoutMs = config.requestTimeoutMs;
236
- let response;
237
- if (requestTimeoutMs === undefined) {
238
- response = await fetchImplementation(url, options);
239
- }
240
- else {
241
- const controller = new AbortController();
242
- const callerSignal = options === null || options === void 0 ? void 0 : options.signal;
243
- let timedOut = false;
244
- const abortFromCaller = () => controller.abort(callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.reason);
245
- const timeout = setTimeout(() => {
246
- timedOut = true;
247
- controller.abort(timeoutError());
248
- }, requestTimeoutMs);
249
- if (callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.aborted) {
250
- abortFromCaller();
251
- }
252
- else {
253
- callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.addEventListener("abort", abortFromCaller, { once: true });
254
- }
255
- try {
256
- response = await fetchImplementation(url, { ...options, signal: controller.signal });
257
- }
258
- catch (error) {
259
- if (timedOut) {
260
- throw timeoutError();
261
- }
262
- throw error;
263
- }
264
- finally {
265
- clearTimeout(timeout);
266
- callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.removeEventListener("abort", abortFromCaller);
267
- }
268
- }
269
- const requestUrl = new URL(url, normalizedBaseUrl);
270
- if (isTokenRefresh(url, options)) {
271
- if (response.status === 400) {
272
- let invalidGrant = false;
273
- try {
274
- invalidGrant = (await response.clone().json()).error === "invalid_grant";
275
- }
276
- catch (_c) {
277
- // Do not sign out unless the response confirms that the refresh token was revoked.
278
- }
279
- if (!invalidGrant) {
280
- throw refreshResponseError(response);
281
- }
282
- }
283
- if ([400, 401, 403].includes(response.status)) {
284
- unauthenticatedReason = {
285
- type: "refresh-rejected",
286
- status: response.status,
287
- };
288
- markActiveAccountExpired();
289
- }
290
- else if (response.ok) {
291
- const accountId = (_b = (_a = storage.getObject("activeLogin")) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
292
- if (accountId) {
293
- (0, account_storage_1.setStoredAccountExpired)(storage, accountId, false);
294
- }
295
- unauthenticatedReason = undefined;
296
- }
297
- }
298
- else if (requestUrl.href === tokenUrl && !response.ok) {
299
- unauthenticatedReason = {
300
- type: "authentication-rejected",
301
- status: response.status,
302
- };
303
- }
304
- else if (response.status === 401) {
305
- unauthenticatedReason = { type: "request-unauthorized", status: 401 };
306
- }
307
- return isTokenRefresh(url, options)
308
- ? response
309
- : withHttpErrorMetadata(response);
310
- };
311
- const fetchWithRefreshRetry = async (url, options) => {
312
- var _a, _b, _c, _d;
313
- const requestUrl = new URL(url, normalizedBaseUrl);
314
- if (!isTokenRefresh(url, options)) {
315
- const response = await fetchOnce(url, options);
316
- const sessionEnded = requestUrl.pathname.endsWith("/auth/session/current") && response.status === 401;
317
- if (sessionEnded) {
318
- markActiveAccountExpired();
319
- }
320
- return response;
321
- }
322
- const login = storage.getObject("activeLogin");
323
- if (terminalRefreshError) {
324
- if ((login === null || login === void 0 ? void 0 : login.accessToken) && login.accessToken !== terminalRefreshToken) {
325
- terminalRefreshToken = undefined;
326
- terminalRefreshError = undefined;
327
- }
328
- else {
329
- throw terminalRefreshError;
330
- }
331
- }
332
- const expiresAt = (login === null || login === void 0 ? void 0 : login.accessToken) ? (0, core_1.tryGetJwtExpiration)(login.accessToken) : undefined;
333
- const signal = options === null || options === void 0 ? void 0 : options.signal;
334
- if (expiresAt !== undefined && options) {
335
- const tokenRequestOptions = options;
336
- tokenRequestOptions.maxRetries = 0;
337
- tokenRequestOptions.maxRetryTime = 0;
338
- }
339
- if (expiresAt === undefined) {
340
- const response = await fetchOnce(url, options);
341
- if (isTemporaryRefreshFailure(response.status)) {
342
- void ((_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel().catch(() => undefined));
343
- if (config.keepSessionOnRefreshUnavailable) {
344
- refreshState.unavailable = true;
345
- }
346
- throw refreshUnavailableError(response);
347
- }
348
- return response;
349
- }
350
- let attempt = 0;
351
- let firstAttempt = true;
352
- let lastResponse;
353
- let lastNetworkError;
354
- while (firstAttempt || Date.now() < expiresAt) {
355
- firstAttempt = false;
356
- try {
357
- lastResponse = await fetchOnce(url, options);
358
- }
359
- catch (error) {
360
- if ((signal === null || signal === void 0 ? void 0 : signal.aborted) || !isNetworkError(error)) {
361
- throw error;
362
- }
363
- if (config.keepSessionOnRefreshUnavailable) {
364
- refreshState.unavailable = true;
365
- lastNetworkError = error;
366
- break;
367
- }
368
- if (Date.now() >= expiresAt) {
369
- lastNetworkError = error;
370
- break;
371
- }
372
- lastNetworkError = error;
373
- const remainingMs = expiresAt - Date.now();
374
- const backoffMs = Math.min(500 * (2 ** Math.min(attempt, 6)), 30000);
375
- await waitForRetry(Math.min(backoffMs, remainingMs), signal);
376
- attempt += 1;
377
- continue;
378
- }
379
- if (!isTemporaryRefreshFailure(lastResponse.status)) {
380
- return lastResponse;
381
- }
382
- if (config.keepSessionOnRefreshUnavailable) {
383
- refreshState.unavailable = true;
384
- break;
385
- }
386
- const remainingMs = expiresAt - Date.now();
387
- if (remainingMs <= 0) {
388
- break;
389
- }
390
- const backoffMs = Math.min(500 * (2 ** Math.min(attempt, 6)), 30000);
391
- const waitMs = Math.min(Math.max((_b = retryAfterMs(lastResponse)) !== null && _b !== void 0 ? _b : 0, backoffMs), remainingMs);
392
- if (waitMs >= remainingMs) {
393
- await waitForRetry(remainingMs, signal);
394
- break;
395
- }
396
- void ((_c = lastResponse.body) === null || _c === void 0 ? void 0 : _c.cancel().catch(() => undefined));
397
- await waitForRetry(waitMs, signal);
398
- attempt += 1;
399
- }
400
- const status = lastResponse === null || lastResponse === void 0 ? void 0 : lastResponse.status;
401
- const reason = {
402
- type: "refresh-unavailable",
403
- ...(status === undefined ? {} : { status }),
404
- };
405
- if (config.keepSessionOnRefreshUnavailable) {
406
- refreshState.unavailable = true;
407
- if (lastResponse) {
408
- return lastResponse;
409
- }
410
- throw lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : refreshUnavailableError(undefined);
411
- }
412
- unauthenticatedReason = reason;
413
- markActiveAccountExpired();
414
- if (lastResponse) {
415
- return lastResponse;
416
- }
417
- terminalRefreshToken = login === null || login === void 0 ? void 0 : login.accessToken;
418
- terminalRefreshError = lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : refreshUnavailableError(undefined);
419
- clearActiveLogin();
420
- (_d = config.onUnauthenticated) === null || _d === void 0 ? void 0 : _d.call(config, reason);
421
- unauthenticatedReason = undefined;
422
- throw terminalRefreshError;
423
- };
86
+ const session = (0, session_fetch_1.createSessionFetch)({ config, storage, fetchImplementation, normalizedBaseUrl, tokenUrl });
424
87
  super({
425
88
  ...config,
426
89
  storage,
427
- onUnauthenticated: () => {
428
- var _a;
429
- if (refreshState.unavailable) {
430
- return;
431
- }
432
- const reason = unauthenticatedReason !== null && unauthenticatedReason !== void 0 ? unauthenticatedReason : {
433
- type: "request-unauthorized",
434
- status: 401,
435
- };
436
- unauthenticatedReason = undefined;
437
- (_a = config.onUnauthenticated) === null || _a === void 0 ? void 0 : _a.call(config, reason);
438
- },
439
- fetch: fetchWithRefreshRetry,
90
+ onUnauthenticated: session.onUnauthenticated,
91
+ fetch: session.fetch,
440
92
  });
441
93
  this.socialLoginClientId = "";
442
- clearActiveLogin = this.clearActiveLogin.bind(this);
94
+ session.setClearActiveLogin(this.clearActiveLogin.bind(this));
443
95
  this.hasExplicitStorageAdapter = config.storage !== undefined;
444
96
  this.clientOptions = config;
445
97
  this.keepSessionOnRefreshUnavailable = config.keepSessionOnRefreshUnavailable === true;
446
- this.refreshState = refreshState;
98
+ this.refreshState = session.refreshState;
447
99
  this.socialLoginClientId = (_e = config.socialLoginClientId) !== null && _e !== void 0 ? _e : "";
448
100
  this.addEventListener("profileRefreshed", () => this.syncSavedAccountProfile());
449
101
  this.bindMethods(authMethods);
@@ -546,7 +198,7 @@ class OvokClient extends core_1.MedplumClient {
546
198
  }
547
199
  catch (error) {
548
200
  const activeLogin = this.getActiveLogin();
549
- if (((_a = activeLogin === null || activeLogin === void 0 ? void 0 : activeLogin.profile) === null || _a === void 0 ? void 0 : _a.reference) === accountId && isNetworkError(error)) {
201
+ if (((_a = activeLogin === null || activeLogin === void 0 ? void 0 : activeLogin.profile) === null || _a === void 0 ? void 0 : _a.reference) === accountId && (0, session_http_1.isNetworkError)(error)) {
550
202
  return;
551
203
  }
552
204
  if (!activeLogin &&
@@ -21,22 +21,13 @@ exports.getPatientDevice = getPatientDevice;
21
21
  exports.listPatientNotifications = listPatientNotifications;
22
22
  exports.getPatientNotification = getPatientNotification;
23
23
  exports.getPatientDashboard = getPatientDashboard;
24
- const query = (params) => {
25
- const values = new URLSearchParams();
26
- for (const [key, value] of Object.entries(params)) {
27
- if (value !== undefined) {
28
- values.set(key, String(value));
29
- }
30
- }
31
- const encoded = values.toString();
32
- return encoded === "" ? "" : `?${encoded}`;
33
- };
24
+ const query_string_1 = require("../query-string");
34
25
  async function getCurrentPatientProfile(requestOptions) {
35
26
  return this.get("/auth/me", ...(requestOptions === undefined ? [] : [requestOptions]));
36
27
  }
37
28
  async function getPatientLatestObservations(params = {}, requestOptions) {
38
29
  const code = Array.isArray(params.code) ? params.code.join(",") : params.code;
39
- return this.get(`/fhir/Observation/$lastn${query({
30
+ return this.get(`/fhir/Observation/$lastn${(0, query_string_1.toQueryString)({
40
31
  patient: params.patient,
41
32
  max: params.max,
42
33
  code,
@@ -6,7 +6,6 @@ exports.listPatientSharing = listPatientSharing;
6
6
  exports.revokePatientSharing = revokePatientSharing;
7
7
  exports.getPatientSharingSettings = getPatientSharingSettings;
8
8
  exports.updatePatientSharingSettings = updatePatientSharingSettings;
9
- const segment = (value) => encodeURIComponent(value);
10
9
  function createPatientSharingInvite(body, requestOptions) {
11
10
  return this.post("/v1/patient-sharing/invites", body, undefined, requestOptions);
12
11
  }
@@ -26,7 +25,7 @@ function listPatientSharing(requestOptions) {
26
25
  });
27
26
  }
28
27
  async function revokePatientSharing(consentId, requestOptions) {
29
- await this.delete(`/v1/patient-sharing/shares/${segment(consentId)}`, ...(requestOptions === undefined ? [] : [requestOptions]));
28
+ await this.delete(`/v1/patient-sharing/shares/${encodeURIComponent(consentId)}`, ...(requestOptions === undefined ? [] : [requestOptions]));
30
29
  this.invalidateUrl("/v1/patient-sharing/shares");
31
30
  }
32
31
  function getPatientSharingSettings(requestOptions) {