@ovok/core 0.4.3 → 0.4.4

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 (54) hide show
  1. package/README.md +182 -4
  2. package/dist/client/auth/errors.d.ts +3 -0
  3. package/dist/client/auth/errors.js +11 -0
  4. package/dist/client/auth/methods/index.d.ts +1 -0
  5. package/dist/client/auth/methods/index.js +1 -0
  6. package/dist/client/auth/methods/registerPractitioner.d.ts +4 -0
  7. package/dist/client/auth/methods/registerPractitioner.js +15 -0
  8. package/dist/client/auth/types/RegisterPractitionerBody.d.ts +2 -0
  9. package/dist/client/auth/types/RegisterPractitionerBody.js +2 -0
  10. package/dist/client/email/index.d.ts +2 -0
  11. package/dist/client/email/index.js +18 -0
  12. package/dist/client/email/methods.d.ts +3 -0
  13. package/dist/client/email/methods.js +6 -0
  14. package/dist/client/email/types.d.ts +21 -0
  15. package/dist/client/email/types.js +2 -0
  16. package/dist/client/errors/ovok-error.d.ts +6 -0
  17. package/dist/client/errors/ovok-error.js +158 -26
  18. package/dist/client/errors/rate-limit-error.js +4 -0
  19. package/dist/client/invites/index.d.ts +3 -0
  20. package/dist/client/invites/index.js +19 -0
  21. package/dist/client/invites/methods.d.ts +8 -0
  22. package/dist/client/invites/methods.js +30 -0
  23. package/dist/client/invites/parse-accept-invite-link.d.ts +3 -0
  24. package/dist/client/invites/parse-accept-invite-link.js +23 -0
  25. package/dist/client/invites/types.d.ts +19 -0
  26. package/dist/client/invites/types.js +8 -0
  27. package/dist/client/ovok-client.d.ts +3 -1
  28. package/dist/client/ovok-client.js +4 -0
  29. package/dist/client/patient-sharing/methods.d.ts +2 -2
  30. package/dist/client/patient-sharing/methods.js +7 -1
  31. package/dist/client/patient-sharing/types.d.ts +30 -3
  32. package/dist/client/platform/methods.d.ts +2 -1
  33. package/dist/client/platform/methods.js +4 -0
  34. package/dist/client/platform/types.d.ts +10 -1
  35. package/dist/client/platform/types.js +15 -2
  36. package/dist/client/signals/errors.js +3 -104
  37. package/dist/client/signals/methods.d.ts +3 -2
  38. package/dist/client/signals/methods.js +3 -2
  39. package/dist/client/signals/types.d.ts +1 -1
  40. package/dist/conformance/api-requirements.js +13 -0
  41. package/dist/conformance/capability-requirements.js +1 -1
  42. package/dist/hooks/email-hooks.d.ts +3 -0
  43. package/dist/hooks/email-hooks.js +46 -0
  44. package/dist/hooks/index.d.ts +3 -1
  45. package/dist/hooks/index.js +9 -2
  46. package/dist/hooks/invites-hooks.d.ts +5 -0
  47. package/dist/hooks/invites-hooks.js +11 -0
  48. package/dist/hooks/patient-sharing-hooks.d.ts +2 -2
  49. package/dist/hooks/platform-hooks.d.ts +5 -1
  50. package/dist/hooks/platform-hooks.js +3 -1
  51. package/dist/index.d.ts +5 -0
  52. package/dist/index.js +3 -0
  53. package/dist/types/ValidationError.d.ts +2 -2
  54. package/package.json +1 -1
package/README.md CHANGED
@@ -75,6 +75,57 @@ const features = await client.getProjectFeatures();
75
75
  const locales = await client.getLocales();
76
76
  ```
77
77
 
78
+ The `values` property contains the six project text settings: `PATIENT_APP_URL`,
79
+ `PRACTITIONER_APP_URL`, `PATIENT_SIGNUP_URL`, `CLINICIAN_DASHBOARD_URL`,
80
+ `DEFAULT_PRACTITIONER_ACCESS_POLICY`, and `CLINICIAN_INVITE_ACCESS_POLICY`. Unset
81
+ values are `null`. Set or clear one with `updateProjectTextSetting(key, value)`;
82
+ pass `null` to clear it. `useUpdateProjectTextSetting()` exposes the same operation
83
+ as a mutation. The `useProjectSettings()` request hook returns the current values.
84
+ Mobile clients should configure `PATIENT_APP_URL` so password-reset and invite
85
+ emails can link back to the patient app. Email links for each audience use that
86
+ audience's configured app URL; they do not use a mobile request's `Origin` header.
87
+
88
+ Any project practitioner can read settings; only a project admin can update text
89
+ values. URL values are trimmed, limited to 2048 characters, and must use HTTPS
90
+ (HTTP is allowed for localhost), an allowed origin, and no credentials, query,
91
+ or fragment. Empty strings are invalid; pass `null` to clear a value. Access
92
+ policies must be `AccessPolicy/<id>` references in the project. The older
93
+ `PATIENT_SIGNUP_URL` and `CLINICIAN_DASHBOARD_URL` settings remain available as
94
+ fallbacks; `PATIENT_SIGNUP_URL` can also be a full continuation link. For an
95
+ audience's base URL, Core checks its project app URL, the parent project's app
96
+ URL, the older setting, the request origin for self-service email only, then
97
+ the configured environment fallback. It never uses a request origin to build a
98
+ link for the other audience. An older Core server may omit `values` and return
99
+ 404 for the write route, so only rely on these methods after the matching Core
100
+ release is deployed.
101
+ The server can return `INVALID_SETTING_VALUE` (422), `APP_URL_NOT_ALLOWED`
102
+ (400), or `UNKNOWN_SETTING` (400; the response lists accepted keys). A code-less
103
+ 409 means settings changed and the screen should reload before retrying. A 403
104
+ means the caller is not a project admin. `readOvokError(error)` reads the status,
105
+ code, user-facing message, and validation field errors from a Medplum error.
106
+ `IOT_ENABLED` is not a supported project setting.
107
+
108
+ The email-template catalog is available to authenticated project practitioners:
109
+
110
+ ```typescript
111
+ const { data: templateCatalog, loading, error, reload } = useEmailTemplates();
112
+ const templates = templateCatalog?.templates;
113
+ ```
114
+
115
+ The equivalent client call is `client.getEmailTemplates()`, which reads
116
+ `GET /v1/email/templates`. Each template includes its name, audience, parameter
117
+ schema, full example, and provider syntax for SendGrid, Brevo, Resend, and SMTP.
118
+ Use this catalog to explain template variables to administrators; it does not
119
+ edit the project's email ConceptMap. Core validates the parameters when an email
120
+ is sent. A send with invalid parameters is refused; SMTP uses strict Handlebars,
121
+ Resend renders nulls as empty strings and lists as newline-delimited values.
122
+ Every template also includes nullable `baseUrl`, `projectName`, and `tenantCode`
123
+ params. The list can grow, so treat template names as strings. The older
124
+ unversioned `/email/templates` route has a different response shape and is not
125
+ used. An older server may return 404 until the matching Core release is
126
+ deployed. The request requires a project practitioner (401 without a session,
127
+ 403 for a non-practitioner).
128
+
78
129
  All methods preserve the backend HTTP verbs and return the backend response shapes. Billing is
79
130
  not included until Ovok publishes a supported billing contract.
80
131
 
@@ -182,10 +233,14 @@ non-empty `reason`; the backend decides whether the caller has the required capa
182
233
  `shared` settings tenant is read-only. Signal errors are exposed as `SignalsError` values and can
183
234
  be checked with `isSignalsError`.
184
235
 
185
- Use `enableSignalsForPatient` to enroll an existing patient. It is idempotent and requires the caller to have
186
- `signals_config:update` and update access to that patient. A `signals_mint_failed` `SignalsError` is a retryable
187
- 503. React applications can call `useEnableSignalsForPatient().mutate(patientId)`; the hook exposes the standard
188
- mutation `loading`, `error`, and `data` state.
236
+ Use `enableSignalsForPatient` to enroll an existing patient. It is idempotent, always returns
237
+ `enabled: true` on success, and requires the caller to have `signals_config:update` and update
238
+ access to that patient. A 409 conflict and 503 failures (including `signals_mint_failed`) are
239
+ retryable; use a bounded retry with backoff and do not retry other statuses automatically. The
240
+ SDK maps backend responses to `SignalsError`; read the `code`, `status`, `fieldErrors`, and
241
+ `message` properties rather than trying to infer a code from a reason phrase. React applications
242
+ can call `useEnableSignalsForPatient().mutate(patientId)`; the hook exposes the standard mutation
243
+ `loading`, `error`, and `data` state.
189
244
 
190
245
  `useSignalsAlerts` accumulates pages through `loadMore`. Check `hasMore` rather than the current
191
246
  item count: access filtering can produce a short or empty page with another cursor. Query changes
@@ -352,6 +407,129 @@ await client.register({ ...registerBody, continueAsPatientToken });
352
407
  The token is optional and is sent only when provided. An invalid or expired token is rejected
353
408
  with HTTP 400 and the message “The continue-as-patient link is invalid or has expired.”
354
409
 
410
+ ### Practitioner self-registration
411
+
412
+ `registerPractitioner()` posts a new practitioner account to
413
+ `POST /auth/tenant/Practitioner/register` and returns an `AuthResponse`. It checks
414
+ `passwordConfirm` locally and never sends that confirmation field. Email is
415
+ lowercased by Core and password must be 1–128 characters. The route is limited to
416
+ five requests per minute, and validation failures return 422 field errors. The project
417
+ must enable `PRACTITIONER_REGISTRATION_ENABLED` and configure a default
418
+ practitioner access policy (prefer `DEFAULT_PRACTITIONER_ACCESS_POLICY`, falling
419
+ back to `CLINICIAN_INVITE_ACCESS_POLICY`). Registration does not verify the
420
+ email address and does not send an email; do not treat the address as verified.
421
+ Core deliberately holds registration failures for at least a second, so a brief
422
+ spinner while the request is pending is expected.
423
+
424
+ ```typescript
425
+ const result = await client.registerPractitioner(registerBody);
426
+ await client.setActiveLogin(result);
427
+ ```
428
+
429
+ Both patient and practitioner registration intentionally return the same
430
+ `400 Registration failed.` when an email already belongs to an account. Show a
431
+ neutral form-level message with sign-in and password-reset links. Do not identify
432
+ the email as taken or retry automatically. `isRegistrationFailed(error)` matches
433
+ this exact response. The practitioner route can also return 403 when registration
434
+ is disabled, 409 `practitioner_registration_not_configured` when no default
435
+ policy is configured, 404 for an unknown tenant, 422 for invalid fields, and 429
436
+ when its five-per-minute limit is reached. `readOvokError(error)` exposes the
437
+ status/code/message and field errors for these responses.
438
+
439
+ ## Patient sharing and invitations
440
+
441
+ Patient sharing settings are read and written through `getPatientSharingSettings()`
442
+ and `updatePatientSharingSettings()`. The update always includes `enabled`; when
443
+ changing only practitioner sharing, send the current `enabled` value too.
444
+ `practitionerSharingEnabled` is a separate, off-by-default setting, not a
445
+ `ProjectSettingKey`. It controls new practitioner invitations and acceptances;
446
+ turning it off does not revoke existing shares. A project admin can toggle it
447
+ through `PUT /v1/patient-sharing/settings`; concurrent settings changes return
448
+ 409 and should be reloaded, and non-admins receive 403. Do not enable it for a project
449
+ whose practitioners use the Carehub dashboard. Map the email templates
450
+ `PATIENT_INVITE`, `PATIENT_ASSIGNMENT_REQUEST`,
451
+ `PRACTITIONER_INVITED_BY_PATIENT`, and `PRACTITIONER_PATIENT_SHARED` before
452
+ turning the feature on; an unmapped email can still produce the uniform 202
453
+ response without sending a message. `useUpdatePatientSharingSettings()` is the
454
+ corresponding mutation hook.
455
+
456
+ `listPatientSharing()` returns `outgoing`, `incoming`, and `practitioners`;
457
+ the latter lists active shares of a patient's whole record with practitioners.
458
+ For compatibility with older servers, the SDK defaults a missing `practitioners`
459
+ array to `[]`. Listing is patient-only (403 for another profile).
460
+ `revokePatientSharing(consentId)` uses the existing DELETE route
461
+ to end a share immediately or decline a pending invite. It returns 204; 404
462
+ (`Share not found.`) means the caller is not a party, and a code-less 409
463
+ (`This share kept changing; try again.`) can be retried after reloading. A
464
+ practitioner's whole-record share has no measurement-code list.
465
+
466
+ Invitations use four client methods, all backed by `/v1/invites/*`:
467
+
468
+ ```typescript
469
+ await client.invitePractitioner({ email, firstName, lastName }); // patient invites practitioner
470
+ await client.invitePatient({ email, firstName, lastName }); // practitioner invites patient
471
+ await client.acceptPractitionerInvite({ inviteId, code });
472
+ await client.acceptPatientInvite({ inviteId, code });
473
+
474
+ // Or dispatch acceptance using the signed-in profile:
475
+ await client.acceptInvite({ inviteId, code });
476
+ ```
477
+
478
+ `useInvitePractitioner()`, `useInvitePatient()`, and `useAcceptInvite()` provide
479
+ React mutation states. The invite methods always return `{ status: "sent" }`
480
+ (HTTP 202), whether or not an email is ultimately sent. Show only “If that
481
+ address can be invited, an email is on its way”; the response must not reveal
482
+ whether the address has an account. Both names are trimmed by Core, and email
483
+ addresses are normalized there. A new address receives an account and a
484
+ set-password link; an existing account receives an accept link. If sending the
485
+ email fails, Core rolls back the invite. Email is limited to 254 characters;
486
+ the trimmed first and last names must each be 1–200 characters.
487
+
488
+ The project must enable `PRACTITIONER_SHARING_ENABLED` and the invitee's
489
+ `PATIENT_INVITATION_ENABLED` or `PRACTITIONER_INVITATION_ENABLED` setting. An
490
+ invite can also fail with 401 (no session), 403 (feature/role gate), 422 (invalid
491
+ body), 409 `invitation_not_configured` (missing default policy),
492
+ `app_url_not_configured` (missing audience app URL), or
493
+ `practitioner_without_access_policy` (a full-access practitioner cannot be
494
+ narrowed to a share policy). A code-less 409 means another invite for the same
495
+ address or practitioner is still being written; offer retry. A 429 means the
496
+ inviter has 20 pending invites. Use
497
+ `readOvokError(error)` and `OVOK_INVITE_ERROR_CODES` to branch on stable codes.
498
+
499
+ ### Accept-invite page
500
+
501
+ Every app that can receive invitations needs a page at
502
+ `<app URL>/accept-invite?invite=<uuid>&code=<secret>`. Practitioner links use
503
+ `PRACTITIONER_APP_URL`; patient links use `PATIENT_APP_URL`. The secret is valid
504
+ for seven days. `parseAcceptInviteLink(url)` returns a body only when the UUID and
505
+ 16–128 character code are present and valid; otherwise show “This link is
506
+ incomplete” and make no request. Once parsed, remove the code from the address
507
+ bar/history and do not send it to logs or analytics.
508
+
509
+ If the visitor is signed out, send them through sign-in and restore the link on
510
+ return. There is no preview endpoint. Once signed in, call `acceptInvite()`; it
511
+ chooses the route from the `Patient` or `Practitioner` profile and rejects other
512
+ profile types. Never accept on page load. For a patient accepting, explain that
513
+ their whole record will be shared and wait for an explicit Accept action. A
514
+ patient can decline with `revokePatientSharing(inviteId)`; practitioners have no
515
+ decline route and can ignore the link. After a successful acceptance, show who
516
+ shares with whom (use a generic name when either is null), provide a way into the
517
+ app, and refresh `/auth/me` because practitioner access may have changed.
518
+
519
+ Render accept failures without attempting to distinguish private cases:
520
+
521
+ - 404 `Invite not found.` means an unknown id, bad code, or invite for another
522
+ account. Say the invite is not for the signed-in account and offer sign-out.
523
+ - 409 `This invite was already used.` means accepted or cancelled; the other
524
+ code-less 409 (`This invite kept changing; try again.`) can be retried.
525
+ - 410 means expired; ask the sender for a new invite.
526
+ - 409 `The patient who shared their record is no longer a member of this
527
+ project.`, `The practitioner who invited you is no longer a member of this
528
+ project.`, or `More than one account in this project acts as that practitioner.`
529
+ cannot be accepted; ask the sender to resolve it.
530
+ - 401 requires sign-in; 403 means the wrong account type or a disabled gate;
531
+ 422 means the link values are invalid.
532
+
355
533
  ## Large measurements
356
534
 
357
535
  `saveMeasurement` and `saveObservations` use conditional creates for idempotent saves. Medplum
@@ -0,0 +1,3 @@
1
+ export declare const REGISTRATION_FAILED_MESSAGE = "Registration failed.";
2
+ /** Returns true when signup was refused because the address already has an account. */
3
+ export declare function isRegistrationFailed(error: unknown): boolean;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REGISTRATION_FAILED_MESSAGE = void 0;
4
+ exports.isRegistrationFailed = isRegistrationFailed;
5
+ const ovok_error_1 = require("../errors/ovok-error");
6
+ exports.REGISTRATION_FAILED_MESSAGE = "Registration failed.";
7
+ /** Returns true when signup was refused because the address already has an account. */
8
+ function isRegistrationFailed(error) {
9
+ const details = (0, ovok_error_1.readOvokError)(error);
10
+ return details.status === 400 && details.message === exports.REGISTRATION_FAILED_MESSAGE;
11
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./login";
2
2
  export * from "./register";
3
+ export * from "./registerPractitioner";
3
4
  export * from "./refetchProfile";
4
5
  export * from "./changePassword";
5
6
  export * from "./requestDeleteUser";
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./login"), exports);
18
18
  __exportStar(require("./register"), exports);
19
+ __exportStar(require("./registerPractitioner"), exports);
19
20
  __exportStar(require("./refetchProfile"), exports);
20
21
  __exportStar(require("./changePassword"), exports);
21
22
  __exportStar(require("./requestDeleteUser"), exports);
@@ -0,0 +1,4 @@
1
+ import { OvokClient } from "../../ovok-client";
2
+ import { AuthResponse } from "../types/AuthResponse";
3
+ import { RegisterPractitionerBody } from "../types/RegisterPractitionerBody";
4
+ export declare function registerPractitioner(this: OvokClient, body: RegisterPractitionerBody): Promise<AuthResponse>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerPractitioner = registerPractitioner;
4
+ async function registerPractitioner(body) {
5
+ if (body.password !== body.passwordConfirm) {
6
+ throw new Error("Password and password confirm do not match");
7
+ }
8
+ return this.post("auth/tenant/Practitioner/register", {
9
+ email: body.email,
10
+ password: body.password,
11
+ name: body.name,
12
+ surname: body.surname,
13
+ tenantCode: body.tenantCode,
14
+ }, "application/json");
15
+ }
@@ -0,0 +1,2 @@
1
+ import { RegisterBody } from "./RegisterBody";
2
+ export type RegisterPractitionerBody = Omit<RegisterBody, "continueAsPatientToken">;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ export * from "./methods";
2
+ export * from "./types";
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./methods"), exports);
18
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,3 @@
1
+ import { OvokClient } from "../ovok-client";
2
+ import { OvokEmailTemplates } from "./types";
3
+ export declare function getEmailTemplates(this: OvokClient): Promise<OvokEmailTemplates>;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getEmailTemplates = getEmailTemplates;
4
+ function getEmailTemplates() {
5
+ return this.get("/v1/email/templates");
6
+ }
@@ -0,0 +1,21 @@
1
+ export type OvokEmailAudience = "patient" | "practitioner";
2
+ export type OvokEmailTemplateParam = {
3
+ name: string;
4
+ type: string;
5
+ nullable: boolean;
6
+ };
7
+ export type OvokEmailTemplate = {
8
+ name: string;
9
+ audience: OvokEmailAudience;
10
+ params: OvokEmailTemplateParam[];
11
+ example: Record<string, unknown>;
12
+ syntax: {
13
+ sendgrid: string;
14
+ brevo: string;
15
+ resend: string;
16
+ smtp: string;
17
+ };
18
+ };
19
+ export type OvokEmailTemplates = {
20
+ templates: OvokEmailTemplate[];
21
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -3,5 +3,11 @@ export type ParsedOvokError = {
3
3
  message: string;
4
4
  fieldErrors: ValidationError[];
5
5
  };
6
+ export type OvokErrorDetails = ParsedOvokError & {
7
+ status: number | undefined;
8
+ code: string | undefined;
9
+ };
10
+ /** Reads the error details Medplum preserves from Ovok's non-FHIR responses. */
11
+ export declare const readOvokError: (error: unknown) => OvokErrorDetails;
6
12
  /** Normalizes Ovok's Nest and FHIR error envelopes without exposing transport details. */
7
13
  export declare const parseOvokError: (error: unknown) => ParsedOvokError;
@@ -1,34 +1,166 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseOvokError = void 0;
4
- const isValidationError = (value) => !!value &&
5
- typeof value === "object" &&
6
- typeof value.code === "string" &&
7
- typeof value.message === "string" &&
8
- Array.isArray(value.path);
3
+ exports.parseOvokError = exports.readOvokError = void 0;
4
+ const STATUS_BY_BARE_CODE = {
5
+ INVALID_SETTING_VALUE: 422,
6
+ APP_URL_NOT_ALLOWED: 400,
7
+ UNKNOWN_SETTING: 400,
8
+ app_url_not_configured: 409,
9
+ };
10
+ const HTTP_REASON_PHRASES = new Set([
11
+ "bad request",
12
+ "unauthorized",
13
+ "forbidden",
14
+ "not found",
15
+ "conflict",
16
+ "too many requests",
17
+ "internal server error",
18
+ "service unavailable",
19
+ ]);
9
20
  const asRecord = (value) => value && typeof value === "object" ? value : undefined;
10
- /** Normalizes Ovok's Nest and FHIR error envelopes without exposing transport details. */
11
- const parseOvokError = (error) => {
12
- var _a, _b;
13
- const parsed = typeof error === "string"
14
- ? (() => {
15
- try {
16
- return JSON.parse(error);
17
- }
18
- catch (_a) {
19
- return undefined;
20
- }
21
- })()
22
- : error;
23
- const root = (_a = asRecord(parsed)) !== null && _a !== void 0 ? _a : {};
24
- const nested = (_b = asRecord(root.error)) !== null && _b !== void 0 ? _b : root;
25
- const rawErrors = nested.errors;
26
- const fieldErrors = Array.isArray(rawErrors) ? rawErrors.filter(isValidationError) : [];
21
+ /** Reads the error details Medplum preserves from Ovok's non-FHIR responses. */
22
+ const readOvokError = (error) => {
23
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
24
+ const root = asRecord(error);
25
+ const response = asRecord(root === null || root === void 0 ? void 0 : root.response);
26
+ const messagePayload = error instanceof Error
27
+ ? parseJson(error.message)
28
+ : parseJson(error);
29
+ const payload = (_f = (_e = (_d = (_c = (_b = (_a = asRecord(messagePayload)) !== null && _a !== void 0 ? _a : asRecord(root === null || root === void 0 ? void 0 : root.body)) !== null && _b !== void 0 ? _b : asRecord(root === null || root === void 0 ? void 0 : root.data)) !== null && _c !== void 0 ? _c : asRecord(response === null || response === void 0 ? void 0 : response.data)) !== null && _d !== void 0 ? _d : asRecord(response === null || response === void 0 ? void 0 : response.body)) !== null && _e !== void 0 ? _e : asRecord(root === null || root === void 0 ? void 0 : root.outcome)) !== null && _f !== void 0 ? _f : root;
30
+ const nestedError = asRecord(payload === null || payload === void 0 ? void 0 : payload.error);
31
+ const bareMessage = typeof error === "string"
32
+ ? error
33
+ : error instanceof Error
34
+ ? error.message
35
+ : undefined;
36
+ const bareCode = bareMessage && /^[A-Za-z][A-Za-z0-9_]*$/.test(bareMessage)
37
+ && !HTTP_REASON_PHRASES.has(bareMessage.toLowerCase())
38
+ ? bareMessage
39
+ : undefined;
40
+ const code = (_k = (_j = (_h = (_g = stringValue(payload === null || payload === void 0 ? void 0 : payload.code)) !== null && _g !== void 0 ? _g : stringValue(nestedError === null || nestedError === void 0 ? void 0 : nestedError.code)) !== null && _h !== void 0 ? _h : codeValue(payload === null || payload === void 0 ? void 0 : payload.error)) !== null && _j !== void 0 ? _j : bareCode) !== null && _k !== void 0 ? _k : codeFromOutcome((_l = payload === null || payload === void 0 ? void 0 : payload.outcome) !== null && _l !== void 0 ? _l : payload);
41
+ const status = (_t = (_r = (_q = (_p = (_o = (_m = numberValue(root === null || root === void 0 ? void 0 : root.status)) !== null && _m !== void 0 ? _m : numberValue(root === null || root === void 0 ? void 0 : root.statusCode)) !== null && _o !== void 0 ? _o : numberValue(response === null || response === void 0 ? void 0 : response.status)) !== null && _p !== void 0 ? _p : numberValue(payload === null || payload === void 0 ? void 0 : payload.status)) !== null && _q !== void 0 ? _q : numberValue(payload === null || payload === void 0 ? void 0 : payload.statusCode)) !== null && _r !== void 0 ? _r : statusFromOutcome((_s = payload === null || payload === void 0 ? void 0 : payload.outcome) !== null && _s !== void 0 ? _s : payload)) !== null && _t !== void 0 ? _t : (code ? STATUS_BY_BARE_CODE[code] : undefined);
42
+ const fieldErrors = readFieldErrors(payload);
27
43
  return {
28
- message: (typeof nested.message === "string" && nested.message) ||
29
- (typeof root.message === "string" && root.message) ||
30
- (error instanceof Error ? error.message : String(error)),
44
+ status,
45
+ code,
46
+ message: readMessage(payload, error),
31
47
  fieldErrors,
32
48
  };
33
49
  };
50
+ exports.readOvokError = readOvokError;
51
+ /** Normalizes Ovok's Nest and FHIR error envelopes without exposing transport details. */
52
+ const parseOvokError = (error) => {
53
+ const { message, fieldErrors } = (0, exports.readOvokError)(error);
54
+ return { message, fieldErrors };
55
+ };
34
56
  exports.parseOvokError = parseOvokError;
57
+ function readMessage(payload, error) {
58
+ var _a, _b, _c;
59
+ const nestedMessage = stringValue((_a = asRecord(payload === null || payload === void 0 ? void 0 : payload.error)) === null || _a === void 0 ? void 0 : _a.message);
60
+ const message = (_c = (_b = stringValue(payload === null || payload === void 0 ? void 0 : payload.message)) !== null && _b !== void 0 ? _b : nestedMessage) !== null && _c !== void 0 ? _c : payload === null || payload === void 0 ? void 0 : payload.message;
61
+ if (typeof message === "string" && message.length > 0) {
62
+ return message;
63
+ }
64
+ if (Array.isArray(message)) {
65
+ const messages = message
66
+ .map((entry) => { var _a; return stringValue((_a = asRecord(entry)) === null || _a === void 0 ? void 0 : _a.message); })
67
+ .filter((entry) => entry !== undefined);
68
+ if (messages.length > 0) {
69
+ return messages.join("; ");
70
+ }
71
+ }
72
+ if (error instanceof Error) {
73
+ return error.message;
74
+ }
75
+ return typeof error === "string" ? error : String(error);
76
+ }
77
+ function readFieldErrors(payload) {
78
+ const messageErrors = Array.isArray(payload === null || payload === void 0 ? void 0 : payload.message) ? payload.message : [];
79
+ const nested = asRecord(payload === null || payload === void 0 ? void 0 : payload.error);
80
+ const rawErrors = Array.isArray(nested === null || nested === void 0 ? void 0 : nested.errors)
81
+ ? nested.errors
82
+ : Array.isArray(payload === null || payload === void 0 ? void 0 : payload.errors)
83
+ ? payload.errors
84
+ : messageErrors;
85
+ return rawErrors.flatMap((value) => {
86
+ const record = asRecord(value);
87
+ if (!record || typeof record.message !== "string") {
88
+ return [];
89
+ }
90
+ const path = Array.isArray(record.path)
91
+ ? record.path.map((segment) => String(segment))
92
+ : [];
93
+ return [{
94
+ ...(stringValue(record.code) ? { code: stringValue(record.code) } : {}),
95
+ message: record.message,
96
+ path,
97
+ ...(stringValue(record.validation)
98
+ ? { validation: stringValue(record.validation) }
99
+ : {}),
100
+ }];
101
+ });
102
+ }
103
+ function codeValue(value) {
104
+ return typeof value === "string" && /^[a-z][a-z0-9_]+$/.test(value)
105
+ ? value
106
+ : undefined;
107
+ }
108
+ function codeFromOutcome(value) {
109
+ var _a, _b;
110
+ const outcome = asRecord(value);
111
+ const issues = Array.isArray(outcome === null || outcome === void 0 ? void 0 : outcome.issue) ? outcome.issue : [];
112
+ for (const issue of issues) {
113
+ const record = asRecord(issue);
114
+ const details = asRecord(record === null || record === void 0 ? void 0 : record.details);
115
+ const coding = Array.isArray(details === null || details === void 0 ? void 0 : details.coding) ? details.coding : [];
116
+ for (const entry of coding) {
117
+ const code = codeValue((_a = asRecord(entry)) === null || _a === void 0 ? void 0 : _a.code);
118
+ if (code === null || code === void 0 ? void 0 : code.startsWith("signals_")) {
119
+ return code;
120
+ }
121
+ }
122
+ const diagnostics = parseJson(record === null || record === void 0 ? void 0 : record.diagnostics);
123
+ const diagnostic = asRecord(diagnostics);
124
+ const code = (_b = codeValue(diagnostic === null || diagnostic === void 0 ? void 0 : diagnostic.error)) !== null && _b !== void 0 ? _b : codeValue(diagnostic === null || diagnostic === void 0 ? void 0 : diagnostic.code);
125
+ if (code) {
126
+ return code;
127
+ }
128
+ }
129
+ return undefined;
130
+ }
131
+ function statusFromOutcome(value) {
132
+ var _a, _b;
133
+ const outcome = asRecord(value);
134
+ const issues = Array.isArray(outcome === null || outcome === void 0 ? void 0 : outcome.issue) ? outcome.issue : [];
135
+ const statusByIssue = {
136
+ conflict: 409,
137
+ forbidden: 403,
138
+ "not-found": 404,
139
+ "too-many-requests": 429,
140
+ throttled: 429,
141
+ };
142
+ for (const issue of issues) {
143
+ const status = statusByIssue[String((_b = (_a = asRecord(issue)) === null || _a === void 0 ? void 0 : _a.code) !== null && _b !== void 0 ? _b : "")];
144
+ if (status) {
145
+ return status;
146
+ }
147
+ }
148
+ return undefined;
149
+ }
150
+ function numberValue(value) {
151
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
152
+ }
153
+ function stringValue(value) {
154
+ return typeof value === "string" && value.length > 0 ? value : undefined;
155
+ }
156
+ function parseJson(value) {
157
+ if (typeof value !== "string") {
158
+ return undefined;
159
+ }
160
+ try {
161
+ return JSON.parse(value);
162
+ }
163
+ catch (_a) {
164
+ return undefined;
165
+ }
166
+ }
@@ -4,6 +4,7 @@ exports.RateLimitError = void 0;
4
4
  exports.isRateLimitError = isRateLimitError;
5
5
  exports.mapRateLimitError = mapRateLimitError;
6
6
  const core_1 = require("@medplum/core");
7
+ const ovok_error_1 = require("./ovok-error");
7
8
  const RATE_LIMIT_RESET_EXTENSION_URL = "https://medplum.com/fhir/StructureDefinition/rate-limit-reset";
8
9
  /** A typed error returned when the server asks the client to slow down. */
9
10
  class RateLimitError extends Error {
@@ -34,6 +35,9 @@ function mapRateLimitError(error) {
34
35
  if (error instanceof core_1.OperationOutcomeError && isRateLimitOutcome(error.outcome)) {
35
36
  return new RateLimitError(rateLimitResetFromOutcome(error.outcome), { cause: error });
36
37
  }
38
+ if ((0, ovok_error_1.readOvokError)(error).status === 429) {
39
+ return new RateLimitError(undefined, { cause: error });
40
+ }
37
41
  return error;
38
42
  }
39
43
  function rateLimitResetFromOutcome(outcome) {
@@ -0,0 +1,3 @@
1
+ export * from "./methods";
2
+ export * from "./parse-accept-invite-link";
3
+ export * from "./types";
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./methods"), exports);
18
+ __exportStar(require("./parse-accept-invite-link"), exports);
19
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,8 @@
1
+ import { OvokClient } from "../ovok-client";
2
+ import { PatientSharingPractitionerShare } from "../patient-sharing/types";
3
+ import { AcceptInviteBody, InviteBody, InviteSent } from "./types";
4
+ export declare function invitePractitioner(this: OvokClient, body: InviteBody): Promise<InviteSent>;
5
+ export declare function invitePatient(this: OvokClient, body: InviteBody): Promise<InviteSent>;
6
+ export declare function acceptPractitionerInvite(this: OvokClient, body: AcceptInviteBody): Promise<PatientSharingPractitionerShare>;
7
+ export declare function acceptPatientInvite(this: OvokClient, body: AcceptInviteBody): Promise<PatientSharingPractitionerShare>;
8
+ export declare function acceptInvite(this: OvokClient, body: AcceptInviteBody): Promise<PatientSharingPractitionerShare>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.invitePractitioner = invitePractitioner;
4
+ exports.invitePatient = invitePatient;
5
+ exports.acceptPractitionerInvite = acceptPractitionerInvite;
6
+ exports.acceptPatientInvite = acceptPatientInvite;
7
+ exports.acceptInvite = acceptInvite;
8
+ function invitePractitioner(body) {
9
+ return this.post("/v1/invites/practitioner", body);
10
+ }
11
+ function invitePatient(body) {
12
+ return this.post("/v1/invites/patient", body);
13
+ }
14
+ function acceptPractitionerInvite(body) {
15
+ return this.post("/v1/invites/practitioner/accept", body);
16
+ }
17
+ function acceptPatientInvite(body) {
18
+ return this.post("/v1/invites/patient/accept", body);
19
+ }
20
+ function acceptInvite(body) {
21
+ var _a;
22
+ const resourceType = (_a = this.getProfile()) === null || _a === void 0 ? void 0 : _a.resourceType;
23
+ if (resourceType === "Practitioner") {
24
+ return this.acceptPractitionerInvite(body);
25
+ }
26
+ if (resourceType === "Patient") {
27
+ return this.acceptPatientInvite(body);
28
+ }
29
+ throw new Error("Accepting an invite requires a signed-in Patient or Practitioner profile.");
30
+ }
@@ -0,0 +1,3 @@
1
+ import { AcceptInviteBody } from "./types";
2
+ /** Reads an invite link without making a request; callers should remove its code from browser history. */
3
+ export declare function parseAcceptInviteLink(url: string | URL): AcceptInviteBody | null;