@ovok/core 0.4.43 → 0.4.45

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 CHANGED
@@ -322,8 +322,8 @@ when `nextCursor` is `null`. Feed `noteCount` is nullable: `null` means the coun
322
322
  zero. Notification notes are practitioner-authored and do not acknowledge or otherwise change an
323
323
  alert. After a note mutation, mounted note and alert-feed hooks reload their data.
324
324
 
325
- Writes can return `OvokApiError`, which keeps the HTTP status, machine code, validation field errors,
326
- and any missing-baseline details. In particular, reload the affected record before retrying a 409;
325
+ Writes can return `OvokApiError`, which keeps the HTTP status, machine code, problem `type`, `traceId`,
326
+ validation field errors (each with its `FIELD_*` code), and any missing-baseline details. In particular, reload the affected record before retrying a 409;
327
327
  do not automatically replay a stale assignment. A 409 `signals_project_migrating` should be shown
328
328
  as a temporary home-migration conflict, not retried in a loop. Threshold writes can return 422
329
329
  validation errors or 424 when the baseline has not been collected. Hooks never replay a write with
@@ -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 request that returns 401 can
450
- clear the session storage and call `onUnauthenticated`. A hook that rendered before sign-in needs
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
- return isTokenRefresh(url, options)
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;
@@ -24,6 +24,22 @@ const retryAfterMs = async (response) => {
24
24
  }
25
25
  };
26
26
  exports.retryAfterMs = retryAfterMs;
27
+ // What `readOvokError` reads from Core's error body: the problem members and the legacy ones kept beside them.
28
+ const PRESERVED_ERROR_KEYS = [
29
+ "type",
30
+ "status",
31
+ "detail",
32
+ "instance",
33
+ "traceId",
34
+ "errors",
35
+ "code",
36
+ "error",
37
+ "message",
38
+ "requestId",
39
+ "retryAfterSec",
40
+ "statusCode",
41
+ "missing",
42
+ ];
27
43
  const timeoutError = () => (0, ovok_error_utils_1.namedError)("TimeoutError", "The request timed out.");
28
44
  const withHttpErrorMetadata = async (response, retry) => {
29
45
  var _a;
@@ -47,7 +63,7 @@ const withHttpErrorMetadata = async (response, retry) => {
47
63
  ? envelope.error
48
64
  : undefined;
49
65
  const safeResponse = envelope
50
- ? Object.fromEntries(Object.entries(envelope).filter(([key]) => ["code", "error", "message", "requestId", "retryAfterSec", "status", "statusCode", "errors", "missing"].includes(key)))
66
+ ? Object.fromEntries(Object.entries(envelope).filter(([key]) => PRESERVED_ERROR_KEYS.includes(key)))
51
67
  : { message: typeof body === "string" ? body : response.statusText };
52
68
  const status = response.status;
53
69
  const retryAfter = retry ? retry.milliseconds : await (0, exports.retryAfterMs)(response);
@@ -1,12 +1,13 @@
1
1
  import type { LoginResponse } from "./LoginResponse";
2
2
  import type { StartLoginResponse } from "./StartLoginResponse";
3
- /** A login selection error with fields apps can branch on without parsing prose. */
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 = 400;
7
+ readonly status: 400 | 401 | 429;
8
8
  readonly requestId: string | undefined;
9
- constructor(code: "profile_not_found" | "tenant_code_required" | "tenant_code_not_found", message: string, profiles?: LoginProfile[], requestId?: string);
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 login selection error with fields apps can branch on without parsing prose. */
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;
@@ -0,0 +1,2 @@
1
+ import { ValidationError } from "../../types/ValidationError";
2
+ export declare function readFieldErrors(payload: Record<string, unknown> | undefined): ValidationError[];
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readFieldErrors = readFieldErrors;
4
+ const ovok_error_utils_1 = require("./ovok-error-utils");
5
+ function readEnvelopeErrors(values) {
6
+ return values.flatMap((value) => {
7
+ const record = (0, ovok_error_utils_1.asRecord)(value);
8
+ if (!record || typeof record.message !== "string") {
9
+ return [];
10
+ }
11
+ const path = Array.isArray(record.path)
12
+ ? record.path.map((segment) => String(segment))
13
+ : [];
14
+ return [{
15
+ ...((0, ovok_error_utils_1.stringValue)(record.code) ? { code: (0, ovok_error_utils_1.stringValue)(record.code) } : {}),
16
+ message: record.message,
17
+ path,
18
+ ...((0, ovok_error_utils_1.stringValue)(record.validation)
19
+ ? { validation: (0, ovok_error_utils_1.stringValue)(record.validation) }
20
+ : {}),
21
+ }];
22
+ });
23
+ }
24
+ const samePath = (left, right) => left.join(".") === right.join(".");
25
+ /** Core's problem `errors[]` (`{ field, code, params }`), worded by the legacy message Core sends for the same path. */
26
+ function readProblemErrors(values, worded) {
27
+ return values.flatMap((value) => {
28
+ var _a, _b;
29
+ const record = (0, ovok_error_utils_1.asRecord)(value);
30
+ const code = (0, ovok_error_utils_1.stringValue)(record === null || record === void 0 ? void 0 : record.code);
31
+ if (!record || !code || typeof record.message === "string" || !("field" in record)) {
32
+ return [];
33
+ }
34
+ const path = typeof record.field === "string" ? record.field.split(".").filter(Boolean) : [];
35
+ const message = (_b = (_a = worded.find((entry) => samePath(entry.path, path))) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : code;
36
+ return [{ code, message, path }];
37
+ });
38
+ }
39
+ function readFieldErrors(payload) {
40
+ const nested = (0, ovok_error_utils_1.asRecord)(payload === null || payload === void 0 ? void 0 : payload.error);
41
+ const listed = Array.isArray(nested === null || nested === void 0 ? void 0 : nested.errors)
42
+ ? nested.errors
43
+ : Array.isArray(payload === null || payload === void 0 ? void 0 : payload.errors)
44
+ ? payload.errors
45
+ : [];
46
+ const legacy = readEnvelopeErrors(Array.isArray(payload === null || payload === void 0 ? void 0 : payload.message) ? payload.message : []);
47
+ const problemErrors = readProblemErrors(listed, legacy);
48
+ const listedErrors = readEnvelopeErrors(listed);
49
+ // A problem body lists its fields in `errors[]` and keeps the older `message` list beside it; read either.
50
+ const envelopeErrors = problemErrors.length > 0
51
+ ? problemErrors
52
+ : listedErrors.length > 0
53
+ ? listedErrors
54
+ : legacy;
55
+ const outcome = Array.isArray(payload === null || payload === void 0 ? void 0 : payload.issue) ? payload.issue : [];
56
+ const nestedEnvelopeErrors = outcome.flatMap((value) => {
57
+ var _a;
58
+ const issue = (0, ovok_error_utils_1.asRecord)(value);
59
+ const details = (0, ovok_error_utils_1.asRecord)(issue === null || issue === void 0 ? void 0 : issue.details);
60
+ const nested = (_a = (0, ovok_error_utils_1.asRecord)((0, ovok_error_utils_1.parseJson)(details === null || details === void 0 ? void 0 : details.text))) !== null && _a !== void 0 ? _a : (0, ovok_error_utils_1.asRecord)((0, ovok_error_utils_1.parseJson)(issue === null || issue === void 0 ? void 0 : issue.diagnostics));
61
+ return nested ? readFieldErrors(nested) : [];
62
+ });
63
+ const outcomeErrors = outcome.flatMap((value) => {
64
+ var _a;
65
+ const issue = (0, ovok_error_utils_1.asRecord)(value);
66
+ const details = (0, ovok_error_utils_1.asRecord)(issue === null || issue === void 0 ? void 0 : issue.details);
67
+ const message = (_a = (0, ovok_error_utils_1.stringValue)(details === null || details === void 0 ? void 0 : details.text)) !== null && _a !== void 0 ? _a : (0, ovok_error_utils_1.stringValue)(issue === null || issue === void 0 ? void 0 : issue.diagnostics);
68
+ if ((issue === null || issue === void 0 ? void 0 : issue.severity) !== "error" || !message) {
69
+ return [];
70
+ }
71
+ const expressions = Array.isArray(issue.expression)
72
+ ? issue.expression
73
+ : Array.isArray(issue.location)
74
+ ? issue.location
75
+ : [];
76
+ const path = expressions.flatMap((expression) => typeof expression === "string" ? expression.split(".").filter(Boolean) : []);
77
+ return [{
78
+ ...((0, ovok_error_utils_1.stringValue)(issue.code) ? { code: (0, ovok_error_utils_1.stringValue)(issue.code) } : {}),
79
+ message,
80
+ path,
81
+ }];
82
+ });
83
+ return [...envelopeErrors, ...nestedEnvelopeErrors, ...outcomeErrors];
84
+ }
@@ -1,4 +1,3 @@
1
- import { ValidationError } from "../../types/ValidationError";
2
1
  export declare const OVOK_HTTP_ERROR_EXTENSION_URL = "https://api.ovok.com/fhir/StructureDefinition/http-error-metadata";
3
2
  /** `reason` itself when it is an Error, else an Error carrying its string form. */
4
3
  export declare const toError: (reason: unknown) => Error;
@@ -8,7 +7,6 @@ export declare const namedError: (name: string, message: string) => Error;
8
7
  export declare const withCause: <T extends Error>(error: T, cause: unknown) => T;
9
8
  export declare const asRecord: (value: unknown) => Record<string, unknown> | undefined;
10
9
  export declare function readMessage(payload: Record<string, unknown> | undefined, error: unknown): string;
11
- export declare function readFieldErrors(payload: Record<string, unknown> | undefined): ValidationError[];
12
10
  export declare function codeValue(value: unknown): string | undefined;
13
11
  export declare function codeFromOutcome(value: unknown): string | undefined;
14
12
  export declare function statusFromOutcome(value: unknown): number | undefined;
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.asRecord = exports.withCause = exports.namedError = exports.toError = exports.OVOK_HTTP_ERROR_EXTENSION_URL = void 0;
4
4
  exports.readMessage = readMessage;
5
- exports.readFieldErrors = readFieldErrors;
6
5
  exports.codeValue = codeValue;
7
6
  exports.codeFromOutcome = codeFromOutcome;
8
7
  exports.statusFromOutcome = statusFromOutcome;
@@ -37,66 +36,15 @@ function readMessage(payload, error) {
37
36
  return messages.join("; ");
38
37
  }
39
38
  }
39
+ const detail = stringValue(payload === null || payload === void 0 ? void 0 : payload.detail);
40
+ if (detail) {
41
+ return detail;
42
+ }
40
43
  if (error instanceof Error) {
41
44
  return error.message;
42
45
  }
43
46
  return typeof error === "string" ? error : String(error);
44
47
  }
45
- function readFieldErrors(payload) {
46
- const messageErrors = Array.isArray(payload === null || payload === void 0 ? void 0 : payload.message) ? payload.message : [];
47
- const nested = (0, exports.asRecord)(payload === null || payload === void 0 ? void 0 : payload.error);
48
- const rawErrors = Array.isArray(nested === null || nested === void 0 ? void 0 : nested.errors)
49
- ? nested.errors
50
- : Array.isArray(payload === null || payload === void 0 ? void 0 : payload.errors)
51
- ? payload.errors
52
- : messageErrors;
53
- const envelopeErrors = rawErrors.flatMap((value) => {
54
- const record = (0, exports.asRecord)(value);
55
- if (!record || typeof record.message !== "string") {
56
- return [];
57
- }
58
- const path = Array.isArray(record.path)
59
- ? record.path.map((segment) => String(segment))
60
- : [];
61
- return [{
62
- ...(stringValue(record.code) ? { code: stringValue(record.code) } : {}),
63
- message: record.message,
64
- path,
65
- ...(stringValue(record.validation)
66
- ? { validation: stringValue(record.validation) }
67
- : {}),
68
- }];
69
- });
70
- const outcome = Array.isArray(payload === null || payload === void 0 ? void 0 : payload.issue) ? payload.issue : [];
71
- const nestedEnvelopeErrors = outcome.flatMap((value) => {
72
- var _a;
73
- const issue = (0, exports.asRecord)(value);
74
- const details = (0, exports.asRecord)(issue === null || issue === void 0 ? void 0 : issue.details);
75
- const nested = (_a = (0, exports.asRecord)(parseJson(details === null || details === void 0 ? void 0 : details.text))) !== null && _a !== void 0 ? _a : (0, exports.asRecord)(parseJson(issue === null || issue === void 0 ? void 0 : issue.diagnostics));
76
- return nested ? readFieldErrors(nested) : [];
77
- });
78
- const outcomeErrors = outcome.flatMap((value) => {
79
- var _a;
80
- const issue = (0, exports.asRecord)(value);
81
- const details = (0, exports.asRecord)(issue === null || issue === void 0 ? void 0 : issue.details);
82
- const message = (_a = stringValue(details === null || details === void 0 ? void 0 : details.text)) !== null && _a !== void 0 ? _a : stringValue(issue === null || issue === void 0 ? void 0 : issue.diagnostics);
83
- if ((issue === null || issue === void 0 ? void 0 : issue.severity) !== "error" || !message) {
84
- return [];
85
- }
86
- const expressions = Array.isArray(issue.expression)
87
- ? issue.expression
88
- : Array.isArray(issue.location)
89
- ? issue.location
90
- : [];
91
- const path = expressions.flatMap((expression) => typeof expression === "string" ? expression.split(".").filter(Boolean) : []);
92
- return [{
93
- ...(stringValue(issue.code) ? { code: stringValue(issue.code) } : {}),
94
- message,
95
- path,
96
- }];
97
- });
98
- return [...envelopeErrors, ...nestedEnvelopeErrors, ...outcomeErrors];
99
- }
100
48
  function codeValue(value) {
101
49
  return typeof value === "string" && /^[a-z][a-z0-9_]+$/.test(value)
102
50
  ? value
@@ -1,12 +1,18 @@
1
1
  import { ValidationError } from "../../types/ValidationError";
2
- export { asRecord, readFieldErrors, readMessage } from "./ovok-error-utils";
2
+ export { asRecord, readMessage } from "./ovok-error-utils";
3
+ export { readFieldErrors } from "./field-errors";
3
4
  export type ParsedOvokError = {
4
5
  message: string;
5
6
  fieldErrors: ValidationError[];
6
7
  };
7
8
  export type OvokErrorDetails = ParsedOvokError & {
8
9
  status: number | undefined;
10
+ /** The condition the server named; undefined when the error is known only by its status. */
9
11
  code: string | undefined;
12
+ /** The problem type URI, `https://docs.ovok.com/errors/<code>`, which also names status-only errors. */
13
+ type?: string;
14
+ /** Equals the `x-trace-id` response header; quote it when reporting an error. */
15
+ traceId?: string;
10
16
  requestId?: string;
11
17
  retryAfterMs?: number;
12
18
  missing: string[];
@@ -17,6 +23,8 @@ export declare const readOvokError: (error: unknown) => OvokErrorDetails;
17
23
  export declare class OvokApiError extends Error {
18
24
  readonly status: number;
19
25
  readonly code: string | undefined;
26
+ readonly type: string | undefined;
27
+ readonly traceId: string | undefined;
20
28
  readonly fieldErrors: ValidationError[];
21
29
  readonly missing: string[];
22
30
  readonly requestId: string | undefined;
@@ -1,13 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseOvokError = exports.OvokApiError = exports.readOvokError = exports.readMessage = exports.readFieldErrors = exports.asRecord = void 0;
3
+ exports.parseOvokError = exports.OvokApiError = exports.readOvokError = exports.readFieldErrors = exports.readMessage = exports.asRecord = void 0;
4
4
  exports.isOvokApiError = isOvokApiError;
5
5
  exports.mapOvokApiError = mapOvokApiError;
6
+ const field_errors_1 = require("./field-errors");
6
7
  const ovok_error_utils_1 = require("./ovok-error-utils");
8
+ const problem_code_1 = require("./problem-code");
7
9
  var ovok_error_utils_2 = require("./ovok-error-utils");
8
10
  Object.defineProperty(exports, "asRecord", { enumerable: true, get: function () { return ovok_error_utils_2.asRecord; } });
9
- Object.defineProperty(exports, "readFieldErrors", { enumerable: true, get: function () { return ovok_error_utils_2.readFieldErrors; } });
10
11
  Object.defineProperty(exports, "readMessage", { enumerable: true, get: function () { return ovok_error_utils_2.readMessage; } });
12
+ var field_errors_2 = require("./field-errors");
13
+ Object.defineProperty(exports, "readFieldErrors", { enumerable: true, get: function () { return field_errors_2.readFieldErrors; } });
11
14
  const STATUS_BY_BARE_CODE = {
12
15
  INVALID_SETTING_VALUE: 422,
13
16
  APP_URL_NOT_ALLOWED: 400,
@@ -31,6 +34,8 @@ const readOvokError = (error) => {
31
34
  return {
32
35
  status: error.status,
33
36
  code: error.code,
37
+ ...(error.type === undefined ? {} : { type: error.type }),
38
+ ...(error.traceId === undefined ? {} : { traceId: error.traceId }),
34
39
  ...(error.requestId === undefined ? {} : { requestId: error.requestId }),
35
40
  ...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }),
36
41
  message: error.message,
@@ -56,9 +61,12 @@ const readOvokError = (error) => {
56
61
  && !HTTP_REASON_PHRASES.has(bareMessage.toLowerCase())
57
62
  ? bareMessage
58
63
  : undefined;
59
- const code = (_l = (_k = (_j = (_h = (0, ovok_error_utils_1.stringValue)(payload === null || payload === void 0 ? void 0 : payload.code)) !== null && _h !== void 0 ? _h : (0, ovok_error_utils_1.stringValue)(nestedError === null || nestedError === void 0 ? void 0 : nestedError.code)) !== null && _j !== void 0 ? _j : (0, ovok_error_utils_1.codeValue)(payload === null || payload === void 0 ? void 0 : payload.error)) !== null && _k !== void 0 ? _k : bareCode) !== null && _l !== void 0 ? _l : (0, ovok_error_utils_1.codeFromOutcome)((_m = payload === null || payload === void 0 ? void 0 : payload.outcome) !== null && _m !== void 0 ? _m : payload);
64
+ const writtenCode = (_h = (0, ovok_error_utils_1.stringValue)(nestedError === null || nestedError === void 0 ? void 0 : nestedError.code)) !== null && _h !== void 0 ? _h : (0, ovok_error_utils_1.codeValue)(payload === null || payload === void 0 ? void 0 : payload.error);
65
+ const code = (_l = (_k = (_j = (0, problem_code_1.conditionCode)((0, problem_code_1.ownCode)(payload === null || payload === void 0 ? void 0 : payload.code), writtenCode)) !== null && _j !== void 0 ? _j : writtenCode) !== null && _k !== void 0 ? _k : bareCode) !== null && _l !== void 0 ? _l : (0, ovok_error_utils_1.codeFromOutcome)((_m = payload === null || payload === void 0 ? void 0 : payload.outcome) !== null && _m !== void 0 ? _m : payload);
66
+ const type = (0, problem_code_1.problemTypeOf)(payload === null || payload === void 0 ? void 0 : payload.type);
67
+ const traceId = (0, ovok_error_utils_1.stringValue)(payload === null || payload === void 0 ? void 0 : payload.traceId);
60
68
  const status = (_v = (_t = (_s = (_r = (_q = (_p = (_o = metadata.status) !== null && _o !== void 0 ? _o : (0, ovok_error_utils_1.numberValue)(root === null || root === void 0 ? void 0 : root.status)) !== null && _p !== void 0 ? _p : (0, ovok_error_utils_1.numberValue)(root === null || root === void 0 ? void 0 : root.statusCode)) !== null && _q !== void 0 ? _q : (0, ovok_error_utils_1.numberValue)(response === null || response === void 0 ? void 0 : response.status)) !== null && _r !== void 0 ? _r : (0, ovok_error_utils_1.numberValue)(payload === null || payload === void 0 ? void 0 : payload.status)) !== null && _s !== void 0 ? _s : (0, ovok_error_utils_1.numberValue)(payload === null || payload === void 0 ? void 0 : payload.statusCode)) !== null && _t !== void 0 ? _t : (0, ovok_error_utils_1.statusFromOutcome)((_u = payload === null || payload === void 0 ? void 0 : payload.outcome) !== null && _u !== void 0 ? _u : payload)) !== null && _v !== void 0 ? _v : (code ? STATUS_BY_BARE_CODE[code] : undefined);
61
- const fieldErrors = (0, ovok_error_utils_1.readFieldErrors)(payload);
69
+ const fieldErrors = (0, field_errors_1.readFieldErrors)(payload);
62
70
  const missing = Array.isArray(payload === null || payload === void 0 ? void 0 : payload.missing)
63
71
  ? payload.missing.filter((entry) => typeof entry === "string")
64
72
  : [];
@@ -68,6 +76,8 @@ const readOvokError = (error) => {
68
76
  return {
69
77
  status,
70
78
  code,
79
+ ...(type === undefined ? {} : { type }),
80
+ ...(traceId === undefined ? {} : { traceId }),
71
81
  ...(requestId === undefined ? {} : { requestId }),
72
82
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
73
83
  message: (0, ovok_error_utils_1.readMessage)(payload, error),
@@ -84,6 +94,8 @@ class OvokApiError extends Error {
84
94
  this.name = "OvokApiError";
85
95
  this.status = (_a = details.status) !== null && _a !== void 0 ? _a : 0;
86
96
  this.code = details.code;
97
+ this.type = details.type;
98
+ this.traceId = details.traceId;
87
99
  this.fieldErrors = details.fieldErrors;
88
100
  this.missing = details.missing;
89
101
  this.requestId = details.requestId;
@@ -0,0 +1,4 @@
1
+ export declare const ownCode: (value: unknown) => string | undefined;
2
+ /** Core sends a throw site's code upper-cased in `code` and as written in `error`: keep the written form. */
3
+ export declare const conditionCode: (problemCode: string | undefined, writtenCode: string | undefined) => string | undefined;
4
+ export declare const problemTypeOf: (value: unknown) => string | undefined;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.problemTypeOf = exports.conditionCode = exports.ownCode = void 0;
4
+ const ovok_error_utils_1 = require("./ovok-error-utils");
5
+ const PROBLEM_TYPE_PREFIX = "https://docs.ovok.com/errors/";
6
+ // The codes Core gives an error by its status alone: they name no condition, so `code` stays undefined for them.
7
+ const STATUS_ONLY_CODES = new Set([
8
+ "MALFORMED_REQUEST",
9
+ "UNAUTHENTICATED",
10
+ "FORBIDDEN",
11
+ "NOT_FOUND",
12
+ "METHOD_NOT_ALLOWED",
13
+ "NOT_ACCEPTABLE",
14
+ "REQUEST_TIMEOUT",
15
+ "CONFLICT",
16
+ "GONE",
17
+ "PRECONDITION_FAILED",
18
+ "CONTENT_TOO_LARGE",
19
+ "UNSUPPORTED_MEDIA_TYPE",
20
+ "VALIDATION_FAILED",
21
+ "LOCKED",
22
+ "FAILED_DEPENDENCY",
23
+ "PRECONDITION_REQUIRED",
24
+ "TOO_MANY_REQUESTS",
25
+ "INTERNAL_ERROR",
26
+ "NOT_IMPLEMENTED",
27
+ "UPSTREAM_ERROR",
28
+ "SERVICE_UNAVAILABLE",
29
+ "UPSTREAM_TIMEOUT",
30
+ "SERVER_ERROR",
31
+ "CLIENT_ERROR",
32
+ ]);
33
+ const ownCode = (value) => {
34
+ const code = (0, ovok_error_utils_1.stringValue)(value);
35
+ return code && !STATUS_ONLY_CODES.has(code) ? code : undefined;
36
+ };
37
+ exports.ownCode = ownCode;
38
+ /** Core sends a throw site's code upper-cased in `code` and as written in `error`: keep the written form. */
39
+ const conditionCode = (problemCode, writtenCode) => problemCode && (writtenCode === null || writtenCode === void 0 ? void 0 : writtenCode.toUpperCase()) === problemCode.toUpperCase() ? writtenCode : problemCode;
40
+ exports.conditionCode = conditionCode;
41
+ const problemTypeOf = (value) => {
42
+ const type = (0, ovok_error_utils_1.stringValue)(value);
43
+ return (type === null || type === void 0 ? void 0 : type.startsWith(PROBLEM_TYPE_PREFIX)) ? type : undefined;
44
+ };
45
+ exports.problemTypeOf = problemTypeOf;
@@ -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.43",
51
+ version: "0.4.45",
52
52
  },
53
53
  fhirVersion: "4.0.1",
54
54
  format: ["json"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovok/core",
3
- "version": "0.4.43",
3
+ "version": "0.4.45",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",