@ovok/core 0.4.30 → 0.4.31
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.
|
@@ -32,6 +32,24 @@ export type SaveObservationsResult = {
|
|
|
32
32
|
queueId: string;
|
|
33
33
|
};
|
|
34
34
|
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
|
+
export type OvokClientOptions = Omit<MedplumClientOptions, "onUnauthenticated"> & {
|
|
49
|
+
onUnauthenticated?: (reason: OvokUnauthenticatedReason) => void;
|
|
50
|
+
socialLoginClientId?: string;
|
|
51
|
+
offlineQueue?: OfflineMeasurementQueueOptions;
|
|
52
|
+
};
|
|
35
53
|
export declare class OvokClient extends MedplumClient {
|
|
36
54
|
protected socialLoginClientId: string;
|
|
37
55
|
clientStorage: IClientStorage | undefined;
|
|
@@ -42,10 +60,7 @@ export declare class OvokClient extends MedplumClient {
|
|
|
42
60
|
* @param config - The configuration for the client.
|
|
43
61
|
* @param config.socialLoginClientId - The social login client id to use for the client.
|
|
44
62
|
*/
|
|
45
|
-
constructor(config:
|
|
46
|
-
socialLoginClientId?: string;
|
|
47
|
-
offlineQueue?: OfflineMeasurementQueueOptions;
|
|
48
|
-
});
|
|
63
|
+
constructor(config: OvokClientOptions);
|
|
49
64
|
/** Lists saved accounts without exposing their access or refresh tokens. */
|
|
50
65
|
getSavedAccounts(): OvokSavedAccount[];
|
|
51
66
|
/** Activates a previously saved account without asking for its password again. */
|
|
@@ -64,6 +64,53 @@ const slimMethods = __importStar(require("./slim/methods"));
|
|
|
64
64
|
exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
65
65
|
var observation_bundle_utils_2 = require("./observation/observation-bundle-utils");
|
|
66
66
|
Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
|
|
67
|
+
const retryAfterMs = (response) => {
|
|
68
|
+
var _a, _b;
|
|
69
|
+
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");
|
|
70
|
+
if (!value) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const seconds = Number(value);
|
|
74
|
+
if (Number.isFinite(seconds)) {
|
|
75
|
+
return Math.max(0, seconds * 1000);
|
|
76
|
+
}
|
|
77
|
+
const date = Date.parse(value);
|
|
78
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
|
|
79
|
+
};
|
|
80
|
+
const waitForRetry = (milliseconds, signal) => new Promise((resolve, reject) => {
|
|
81
|
+
var _a;
|
|
82
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
83
|
+
const error = new Error("Token refresh aborted");
|
|
84
|
+
error.name = "AbortError";
|
|
85
|
+
reject((_a = signal.reason) !== null && _a !== void 0 ? _a : error);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener("abort", abort);
|
|
90
|
+
resolve();
|
|
91
|
+
}, milliseconds);
|
|
92
|
+
const abort = () => {
|
|
93
|
+
var _a;
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
const error = new Error("Token refresh aborted");
|
|
96
|
+
error.name = "AbortError";
|
|
97
|
+
reject((_a = signal === null || signal === void 0 ? void 0 : signal.reason) !== null && _a !== void 0 ? _a : error);
|
|
98
|
+
};
|
|
99
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener("abort", abort, { once: true });
|
|
100
|
+
});
|
|
101
|
+
const isTemporaryRefreshFailure = (status) => status === 429 || (status >= 500 && status <= 599);
|
|
102
|
+
const refreshUnavailableError = (response) => {
|
|
103
|
+
const error = new Error("Token refresh is temporarily unavailable");
|
|
104
|
+
error.name = "NetworkError";
|
|
105
|
+
Object.assign(error, { cause: response });
|
|
106
|
+
return error;
|
|
107
|
+
};
|
|
108
|
+
const refreshResponseError = (response) => {
|
|
109
|
+
const error = new Error(`Token refresh returned HTTP ${response.status}`);
|
|
110
|
+
error.name = "TokenRefreshError";
|
|
111
|
+
Object.assign(error, { status: response.status, cause: response });
|
|
112
|
+
return error;
|
|
113
|
+
};
|
|
67
114
|
const isNetworkError = (error) => {
|
|
68
115
|
if (!(error instanceof Error)) {
|
|
69
116
|
return false;
|
|
@@ -87,25 +134,174 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
87
134
|
const baseUrl = (_c = config.baseUrl) !== null && _c !== void 0 ? _c : "https://api.medplum.com/";
|
|
88
135
|
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
89
136
|
const tokenUrl = new URL((_d = config.tokenUrl) !== null && _d !== void 0 ? _d : "oauth2/token", normalizedBaseUrl).href;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
137
|
+
let unauthenticatedReason;
|
|
138
|
+
let clearActiveLogin = () => undefined;
|
|
139
|
+
let terminalRefreshToken;
|
|
140
|
+
let terminalRefreshError;
|
|
141
|
+
const isTokenRefresh = (url, options) => new URL(url, normalizedBaseUrl).href === tokenUrl &&
|
|
142
|
+
(options === null || options === void 0 ? void 0 : options.method) === "POST" &&
|
|
143
|
+
typeof options.body === "string" &&
|
|
144
|
+
new URLSearchParams(options.body).get("grant_type") === "refresh_token";
|
|
145
|
+
const markActiveAccountExpired = () => {
|
|
146
|
+
var _a, _b;
|
|
147
|
+
const accountId = (_b = (_a = storage.getObject("activeLogin")) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
|
|
148
|
+
if (accountId) {
|
|
149
|
+
(0, account_storage_1.setStoredAccountExpired)(storage, accountId, true);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const fetchOnce = async (url, options) => {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
const response = await fetchImplementation(url, options);
|
|
155
|
+
const requestUrl = new URL(url, normalizedBaseUrl);
|
|
156
|
+
if (isTokenRefresh(url, options)) {
|
|
157
|
+
if (response.status === 400) {
|
|
158
|
+
let invalidGrant = false;
|
|
159
|
+
try {
|
|
160
|
+
invalidGrant = (await response.clone().json()).error === "invalid_grant";
|
|
161
|
+
}
|
|
162
|
+
catch (_c) {
|
|
163
|
+
// Do not sign out unless the response confirms that the refresh token was revoked.
|
|
164
|
+
}
|
|
165
|
+
if (!invalidGrant) {
|
|
166
|
+
throw refreshResponseError(response);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if ([400, 401, 403].includes(response.status)) {
|
|
170
|
+
unauthenticatedReason = {
|
|
171
|
+
type: "refresh-rejected",
|
|
172
|
+
status: response.status,
|
|
173
|
+
};
|
|
174
|
+
markActiveAccountExpired();
|
|
175
|
+
}
|
|
176
|
+
else if (response.ok) {
|
|
100
177
|
const accountId = (_b = (_a = storage.getObject("activeLogin")) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
|
|
101
178
|
if (accountId) {
|
|
102
|
-
(0, account_storage_1.setStoredAccountExpired)(storage, accountId,
|
|
179
|
+
(0, account_storage_1.setStoredAccountExpired)(storage, accountId, false);
|
|
103
180
|
}
|
|
181
|
+
unauthenticatedReason = undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (requestUrl.href === tokenUrl && !response.ok) {
|
|
185
|
+
unauthenticatedReason = {
|
|
186
|
+
type: "authentication-rejected",
|
|
187
|
+
status: response.status,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
else if (response.status === 401) {
|
|
191
|
+
unauthenticatedReason = { type: "request-unauthorized", status: 401 };
|
|
192
|
+
}
|
|
193
|
+
return response;
|
|
194
|
+
};
|
|
195
|
+
const fetchWithRefreshRetry = async (url, options) => {
|
|
196
|
+
var _a, _b, _c, _d;
|
|
197
|
+
const requestUrl = new URL(url, normalizedBaseUrl);
|
|
198
|
+
if (!isTokenRefresh(url, options)) {
|
|
199
|
+
const response = await fetchOnce(url, options);
|
|
200
|
+
const sessionEnded = requestUrl.pathname.endsWith("/auth/session/current") && response.status === 401;
|
|
201
|
+
if (sessionEnded) {
|
|
202
|
+
markActiveAccountExpired();
|
|
104
203
|
}
|
|
105
204
|
return response;
|
|
205
|
+
}
|
|
206
|
+
const login = storage.getObject("activeLogin");
|
|
207
|
+
if (terminalRefreshError) {
|
|
208
|
+
if ((login === null || login === void 0 ? void 0 : login.accessToken) && login.accessToken !== terminalRefreshToken) {
|
|
209
|
+
terminalRefreshToken = undefined;
|
|
210
|
+
terminalRefreshError = undefined;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
throw terminalRefreshError;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const expiresAt = (login === null || login === void 0 ? void 0 : login.accessToken) ? (0, core_1.tryGetJwtExpiration)(login.accessToken) : undefined;
|
|
217
|
+
const signal = options === null || options === void 0 ? void 0 : options.signal;
|
|
218
|
+
if (expiresAt !== undefined && options) {
|
|
219
|
+
const tokenRequestOptions = options;
|
|
220
|
+
tokenRequestOptions.maxRetries = 0;
|
|
221
|
+
tokenRequestOptions.maxRetryTime = 0;
|
|
222
|
+
}
|
|
223
|
+
if (expiresAt === undefined) {
|
|
224
|
+
const response = await fetchOnce(url, options);
|
|
225
|
+
if (isTemporaryRefreshFailure(response.status)) {
|
|
226
|
+
void ((_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel().catch(() => undefined));
|
|
227
|
+
throw refreshUnavailableError(response);
|
|
228
|
+
}
|
|
229
|
+
return response;
|
|
230
|
+
}
|
|
231
|
+
let attempt = 0;
|
|
232
|
+
let firstAttempt = true;
|
|
233
|
+
let lastResponse;
|
|
234
|
+
let lastNetworkError;
|
|
235
|
+
while (firstAttempt || Date.now() < expiresAt) {
|
|
236
|
+
firstAttempt = false;
|
|
237
|
+
try {
|
|
238
|
+
lastResponse = await fetchOnce(url, options);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if ((signal === null || signal === void 0 ? void 0 : signal.aborted) || !isNetworkError(error)) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
if (Date.now() >= expiresAt) {
|
|
245
|
+
lastNetworkError = error;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
lastNetworkError = error;
|
|
249
|
+
const remainingMs = expiresAt - Date.now();
|
|
250
|
+
const backoffMs = Math.min(500 * (2 ** Math.min(attempt, 6)), 30000);
|
|
251
|
+
await waitForRetry(Math.min(backoffMs, remainingMs), signal);
|
|
252
|
+
attempt += 1;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!isTemporaryRefreshFailure(lastResponse.status)) {
|
|
256
|
+
return lastResponse;
|
|
257
|
+
}
|
|
258
|
+
const remainingMs = expiresAt - Date.now();
|
|
259
|
+
if (remainingMs <= 0) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
const backoffMs = Math.min(500 * (2 ** Math.min(attempt, 6)), 30000);
|
|
263
|
+
const waitMs = Math.min(Math.max((_b = retryAfterMs(lastResponse)) !== null && _b !== void 0 ? _b : 0, backoffMs), remainingMs);
|
|
264
|
+
if (waitMs >= remainingMs) {
|
|
265
|
+
await waitForRetry(remainingMs, signal);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
void ((_c = lastResponse.body) === null || _c === void 0 ? void 0 : _c.cancel().catch(() => undefined));
|
|
269
|
+
await waitForRetry(waitMs, signal);
|
|
270
|
+
attempt += 1;
|
|
271
|
+
}
|
|
272
|
+
const status = lastResponse === null || lastResponse === void 0 ? void 0 : lastResponse.status;
|
|
273
|
+
const reason = {
|
|
274
|
+
type: "refresh-unavailable",
|
|
275
|
+
...(status === undefined ? {} : { status }),
|
|
276
|
+
};
|
|
277
|
+
unauthenticatedReason = reason;
|
|
278
|
+
markActiveAccountExpired();
|
|
279
|
+
if (lastResponse) {
|
|
280
|
+
return lastResponse;
|
|
281
|
+
}
|
|
282
|
+
terminalRefreshToken = login === null || login === void 0 ? void 0 : login.accessToken;
|
|
283
|
+
terminalRefreshError = lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : refreshUnavailableError(undefined);
|
|
284
|
+
clearActiveLogin();
|
|
285
|
+
(_d = config.onUnauthenticated) === null || _d === void 0 ? void 0 : _d.call(config, reason);
|
|
286
|
+
unauthenticatedReason = undefined;
|
|
287
|
+
throw terminalRefreshError;
|
|
288
|
+
};
|
|
289
|
+
super({
|
|
290
|
+
...config,
|
|
291
|
+
storage,
|
|
292
|
+
onUnauthenticated: () => {
|
|
293
|
+
var _a;
|
|
294
|
+
const reason = unauthenticatedReason !== null && unauthenticatedReason !== void 0 ? unauthenticatedReason : {
|
|
295
|
+
type: "request-unauthorized",
|
|
296
|
+
status: 401,
|
|
297
|
+
};
|
|
298
|
+
unauthenticatedReason = undefined;
|
|
299
|
+
(_a = config.onUnauthenticated) === null || _a === void 0 ? void 0 : _a.call(config, reason);
|
|
106
300
|
},
|
|
301
|
+
fetch: fetchWithRefreshRetry,
|
|
107
302
|
});
|
|
108
303
|
this.socialLoginClientId = "";
|
|
304
|
+
clearActiveLogin = this.clearActiveLogin.bind(this);
|
|
109
305
|
this.hasExplicitStorageAdapter = config.storage !== undefined;
|
|
110
306
|
this.socialLoginClientId = (_e = config.socialLoginClientId) !== null && _e !== void 0 ? _e : "";
|
|
111
307
|
this.addEventListener("profileRefreshed", () => this.syncSavedAccountProfile());
|
|
@@ -48,7 +48,7 @@ exports.OVOK_CORE_REQUIREMENTS = {
|
|
|
48
48
|
description: "The FHIR surface a server must provide for the @ovok/core client library's own methods to function.",
|
|
49
49
|
software: {
|
|
50
50
|
name: "@ovok/core",
|
|
51
|
-
version: "0.4.
|
|
51
|
+
version: "0.4.31",
|
|
52
52
|
},
|
|
53
53
|
fhirVersion: "4.0.1",
|
|
54
54
|
format: ["json"],
|
package/dist/index.d.ts
CHANGED
|
@@ -923,6 +923,7 @@ declare const Medplum: {
|
|
|
923
923
|
};
|
|
924
924
|
export { Medplum };
|
|
925
925
|
export { OvokClient } from "./client/ovok-client";
|
|
926
|
+
export type { OvokClientOptions, OvokUnauthenticatedReason } from "./client/ovok-client";
|
|
926
927
|
export * from "./client/errors/rate-limit-error";
|
|
927
928
|
export * from "./client/errors/sign-in-required-error";
|
|
928
929
|
export * from "./client/errors/ovok-error";
|