@ovok/core 0.4.43 → 0.4.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/client/auth/session-fetch.js +33 -3
- package/dist/client/auth/types/LoginError.d.ts +5 -4
- package/dist/client/auth/types/LoginError.js +4 -3
- package/dist/client/ovok-client.js +2 -1
- package/dist/conformance/capability-requirements.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -446,8 +446,9 @@ The backend allows three kinds of read:
|
|
|
446
446
|
Signed out, a read rejects with `SignInRequiredError` (`isSignInRequiredError(error)`) before
|
|
447
447
|
any request is sent, unless it passes `allowSignedOut: true` and a `tenantCode`. `getLegalPage`,
|
|
448
448
|
`getTranslations` and their hooks turn `allowSignedOut` on by default; pass it yourself only for
|
|
449
|
-
another collection the backend serves without a key or session. A
|
|
450
|
-
clear the session storage and call `onUnauthenticated
|
|
449
|
+
another collection the backend serves without a key or session. A 401 from an ordinary request can
|
|
450
|
+
clear the session storage and call `onUnauthenticated`; a 401 from password sign-in is instead a
|
|
451
|
+
`LoginError` and leaves the active account untouched. A hook that rendered before sign-in needs
|
|
451
452
|
`reload()` once the session exists.
|
|
452
453
|
|
|
453
454
|
`locale` takes an app language or a CMS locale (`en`, `de`, `fr`, `es`). `toCmsLocale` maps
|
|
@@ -541,6 +542,14 @@ if ("nextStep" in result && result.nextStep === "mfa") {
|
|
|
541
542
|
}
|
|
542
543
|
```
|
|
543
544
|
|
|
545
|
+
### Password sign-in errors
|
|
546
|
+
|
|
547
|
+
Password sign-in failures throw `LoginError`. A 401 has `code: "invalid_credentials"` and the
|
|
548
|
+
server's message; it does not refresh tokens, clear an existing account, or call
|
|
549
|
+
`onUnauthenticated`. A 429 has `code: "rate_limited"` and includes `retryAfterMs` when the server
|
|
550
|
+
provides a retry delay. Show the error or a wait message to the user, then honor that delay before
|
|
551
|
+
another attempt. The backend controls the lockout threshold and duration.
|
|
552
|
+
|
|
544
553
|
## Patient registration invitations
|
|
545
554
|
|
|
546
555
|
When a project invites people with a business email address to join as clinicians,
|
|
@@ -3,8 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createSessionFetch = createSessionFetch;
|
|
4
4
|
/* eslint-disable max-lines -- one refresh state machine; splitting it would scatter its shared session state. */
|
|
5
5
|
const core_1 = require("@medplum/core");
|
|
6
|
+
const ovok_error_1 = require("../errors/ovok-error");
|
|
6
7
|
const account_storage_1 = require("./account-storage");
|
|
7
8
|
const session_http_1 = require("./session-http");
|
|
9
|
+
const LoginError_1 = require("./types/LoginError");
|
|
10
|
+
/** These POSTs authenticate a new session; a failed attempt must not invalidate an existing one. */
|
|
11
|
+
const isSignInRequest = (url, options, normalizedBaseUrl) => {
|
|
12
|
+
var _a;
|
|
13
|
+
if (((_a = options === null || options === void 0 ? void 0 : options.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== "POST") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const baseUrl = new URL(normalizedBaseUrl);
|
|
17
|
+
const requestUrl = new URL(url, normalizedBaseUrl);
|
|
18
|
+
if (requestUrl.origin !== baseUrl.origin) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const path = (requestUrl.pathname.startsWith(baseUrl.pathname)
|
|
22
|
+
? requestUrl.pathname.slice(baseUrl.pathname.length)
|
|
23
|
+
: requestUrl.pathname.slice(1)).replace(/^\/+|\/+$/g, "");
|
|
24
|
+
return [
|
|
25
|
+
/^auth\/login$/,
|
|
26
|
+
/^v2\/auth\/login$/,
|
|
27
|
+
/^auth\/tenant\/(?:Patient|Practitioner)\/login\/(?:start|mfa|token)$/,
|
|
28
|
+
/^v1\/saas\/login(?:\/token)?$/,
|
|
29
|
+
].some((route) => route.test(path));
|
|
30
|
+
};
|
|
8
31
|
/**
|
|
9
32
|
* The client's `fetch`: request timeouts, token-refresh retries within the access token's
|
|
10
33
|
* lifetime, and tracking why a session ended, which `onUnauthenticated` reports.
|
|
@@ -28,6 +51,7 @@ function createSessionFetch({ config, storage, fetchImplementation, normalizedBa
|
|
|
28
51
|
};
|
|
29
52
|
const fetchOnce = async (url, options) => {
|
|
30
53
|
const response = await (0, session_http_1.fetchWithTimeout)(fetchImplementation, url, options, config.requestTimeoutMs);
|
|
54
|
+
const signInRequest = isSignInRequest(url, options, normalizedBaseUrl);
|
|
31
55
|
const retry = !response.ok && !isTokenRefresh(url, options)
|
|
32
56
|
? { milliseconds: await (0, session_http_1.retryAfterMs)(response) }
|
|
33
57
|
: undefined;
|
|
@@ -71,12 +95,18 @@ function createSessionFetch({ config, storage, fetchImplementation, normalizedBa
|
|
|
71
95
|
status: response.status,
|
|
72
96
|
};
|
|
73
97
|
}
|
|
74
|
-
else if (response.status === 401) {
|
|
98
|
+
else if (response.status === 401 && !signInRequest) {
|
|
75
99
|
unauthenticatedReason = { type: "request-unauthorized", status: 401 };
|
|
76
100
|
}
|
|
77
|
-
|
|
101
|
+
const responseWithMetadata = isTokenRefresh(url, options)
|
|
78
102
|
? response
|
|
79
|
-
: (0, session_http_1.withHttpErrorMetadata)(response, retry);
|
|
103
|
+
: await (0, session_http_1.withHttpErrorMetadata)(response, retry);
|
|
104
|
+
if (signInRequest && (response.status === 401 || response.status === 429)) {
|
|
105
|
+
const status = response.status;
|
|
106
|
+
const details = (0, ovok_error_1.readOvokError)({ outcome: await responseWithMetadata.clone().json() });
|
|
107
|
+
throw new LoginError_1.LoginError(status === 401 ? "invalid_credentials" : "rate_limited", details.message, [], details.requestId, status, details.retryAfterMs);
|
|
108
|
+
}
|
|
109
|
+
return responseWithMetadata;
|
|
80
110
|
};
|
|
81
111
|
const fetchWithRefreshRetry = async (url, options) => {
|
|
82
112
|
var _a, _b, _c, _d, _e;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { LoginResponse } from "./LoginResponse";
|
|
2
2
|
import type { StartLoginResponse } from "./StartLoginResponse";
|
|
3
|
-
/** A
|
|
3
|
+
/** A typed sign-in error with fields apps can branch on without parsing prose. */
|
|
4
4
|
export declare class LoginError extends Error {
|
|
5
|
-
readonly code: "profile_not_found" | "tenant_code_required" | "tenant_code_not_found";
|
|
5
|
+
readonly code: "profile_not_found" | "tenant_code_required" | "tenant_code_not_found" | "invalid_credentials" | "rate_limited";
|
|
6
6
|
readonly profiles: LoginProfile[];
|
|
7
|
-
readonly status
|
|
7
|
+
readonly status: 400 | 401 | 429;
|
|
8
8
|
readonly requestId: string | undefined;
|
|
9
|
-
|
|
9
|
+
readonly retryAfterMs: number | undefined;
|
|
10
|
+
constructor(code: "profile_not_found" | "tenant_code_required" | "tenant_code_not_found" | "invalid_credentials" | "rate_limited", message: string, profiles?: LoginProfile[], requestId?: string, status?: 400 | 401 | 429, retryAfterMs?: number);
|
|
10
11
|
}
|
|
11
12
|
export type LoginProfile = NonNullable<StartLoginResponse["profiles"]>[number];
|
|
12
13
|
export type TenantLoginStart = {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LoginError = void 0;
|
|
4
|
-
/** A
|
|
4
|
+
/** A typed sign-in error with fields apps can branch on without parsing prose. */
|
|
5
5
|
class LoginError extends Error {
|
|
6
|
-
constructor(code, message, profiles = [], requestId) {
|
|
6
|
+
constructor(code, message, profiles = [], requestId, status = 400, retryAfterMs) {
|
|
7
7
|
super(message);
|
|
8
8
|
this.code = code;
|
|
9
9
|
this.profiles = profiles;
|
|
10
|
-
this.status = 400;
|
|
11
10
|
this.name = "LoginError";
|
|
11
|
+
this.status = status;
|
|
12
12
|
this.requestId = requestId;
|
|
13
|
+
this.retryAfterMs = retryAfterMs;
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
exports.LoginError = LoginError;
|
|
@@ -43,6 +43,7 @@ const errors_1 = require("./auth/errors");
|
|
|
43
43
|
const authMethods = __importStar(require("./auth/methods"));
|
|
44
44
|
const session_fetch_1 = require("./auth/session-fetch");
|
|
45
45
|
const session_http_1 = require("./auth/session-http");
|
|
46
|
+
const LoginError_1 = require("./auth/types/LoginError");
|
|
46
47
|
const botMethods = __importStar(require("./bot/methods"));
|
|
47
48
|
const changeHistoryMethods = __importStar(require("./change-history/methods"));
|
|
48
49
|
const aiMethods = __importStar(require("./chat/ai/methods"));
|
|
@@ -125,7 +126,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
125
126
|
this.refreshState = session.refreshState;
|
|
126
127
|
this.socialLoginClientId = (_e = config.socialLoginClientId) !== null && _e !== void 0 ? _e : "";
|
|
127
128
|
this.addEventListener("profileRefreshed", () => this.syncSavedAccountProfile());
|
|
128
|
-
this.bindMethods(authMethods);
|
|
129
|
+
this.bindMethods(authMethods, (error) => error instanceof LoginError_1.LoginError ? error : this.mapError(error));
|
|
129
130
|
this.bindMethods(observationMethods);
|
|
130
131
|
this.bindMethods(questionnaireResponseMethods);
|
|
131
132
|
this.bindMethods(aiMethods);
|
|
@@ -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.44",
|
|
52
52
|
},
|
|
53
53
|
fhirVersion: "4.0.1",
|
|
54
54
|
format: ["json"],
|