@ovok/core 0.4.3 → 0.4.5

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 +221 -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 +17 -4
  38. package/dist/client/signals/methods.js +47 -24
  39. package/dist/client/signals/types.d.ts +74 -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,16 +233,59 @@ 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
192
247
  and `reload` start again at the first page. The hook exposes `reload` so a screen can refresh its
193
248
  alert list after `useAcknowledgeSignalsAlert` succeeds.
194
249
 
250
+ ### Raw Signals alerts
251
+
252
+ `listSignalsAlerts` reads episodes and needs the project's `episodicAlerts` (409
253
+ `signals_episodes_disabled` otherwise). To work with individual alerts instead, whether or not
254
+ `episodicAlerts` is on, use the raw alert methods:
255
+
256
+ ```ts
257
+ const open = await client.listSignalsRawAlerts({
258
+ acknowledged: false,
259
+ codes: ["8867-4", "9279-1"],
260
+ since: "2026-10-01T00:00:00Z",
261
+ limit: 50,
262
+ });
263
+ const alert = await client.getSignalsRawAlert(open.items[0].id);
264
+ const acked = await client.acknowledgeSignalsRawAlert(alert.id);
265
+ // acked.acknowledgedAt and acked.acknowledgedActor record when and by whom.
266
+ ```
267
+
268
+ Each `SignalsRawAlert` carries Signals' own `id`, `createdAt` and `resolvedAt`. `status` is Signals'
269
+ state of the reading: `ACTIVE` while it breaches, `RESOLVED` once it recovered or the alert was
270
+ acknowledged. An alert stays open for the practitioner until it is acknowledged, so list the
271
+ worklist with `acknowledged: false` rather than `status: "ACTIVE"`. Acknowledging resolves the
272
+ alert, and the next breaching reading opens a new one. It is idempotent: an acknowledged alert keeps
273
+ its first `acknowledgedAt` and `acknowledgedActor`.
274
+
275
+ Filters: `status`, `acknowledged`, `patientId` (a Medplum Patient id), `codes` (LOINC codes), and
276
+ `since` (inclusive) and `until` (exclusive) on `createdAt`, as date-times with `Z` or an offset.
277
+ Pages hold at most 50 alerts. As with episodes, a page can be short or empty and still have a
278
+ `nextCursor`, because alerts of residents the caller can't read are left out; page until
279
+ `nextCursor` is null, and pass it back unchanged: it is sealed to the project.
280
+
281
+ Alerts raised before a project starts acknowledging are unacknowledged too, so a new worklist
282
+ should bound its history with `since`. Each row also carries the breaching reading (`valueNumber`
283
+ or `valueString`), the rule's `message` and the alert's `reason`. Acknowledging a raw alert does not
284
+ acknowledge an episode: with `episodicAlerts` on, keep a screen to one of the two. Every request is scoped to the
285
+ caller's project and the residents their access policy allows: an alert outside that scope answers
286
+ 404, and acknowledging needs `signals_alerts:update` and update access to the resident (403 when the
287
+ caller can only read them).
288
+
195
289
  ## CareHub APIs
196
290
 
197
291
  CareHub methods cover the verified `/v1/slim/*` and `/v2/slim/*` application routes for
@@ -352,6 +446,129 @@ await client.register({ ...registerBody, continueAsPatientToken });
352
446
  The token is optional and is sent only when provided. An invalid or expired token is rejected
353
447
  with HTTP 400 and the message “The continue-as-patient link is invalid or has expired.”
354
448
 
449
+ ### Practitioner self-registration
450
+
451
+ `registerPractitioner()` posts a new practitioner account to
452
+ `POST /auth/tenant/Practitioner/register` and returns an `AuthResponse`. It checks
453
+ `passwordConfirm` locally and never sends that confirmation field. Email is
454
+ lowercased by Core and password must be 1–128 characters. The route is limited to
455
+ five requests per minute, and validation failures return 422 field errors. The project
456
+ must enable `PRACTITIONER_REGISTRATION_ENABLED` and configure a default
457
+ practitioner access policy (prefer `DEFAULT_PRACTITIONER_ACCESS_POLICY`, falling
458
+ back to `CLINICIAN_INVITE_ACCESS_POLICY`). Registration does not verify the
459
+ email address and does not send an email; do not treat the address as verified.
460
+ Core deliberately holds registration failures for at least a second, so a brief
461
+ spinner while the request is pending is expected.
462
+
463
+ ```typescript
464
+ const result = await client.registerPractitioner(registerBody);
465
+ await client.setActiveLogin(result);
466
+ ```
467
+
468
+ Both patient and practitioner registration intentionally return the same
469
+ `400 Registration failed.` when an email already belongs to an account. Show a
470
+ neutral form-level message with sign-in and password-reset links. Do not identify
471
+ the email as taken or retry automatically. `isRegistrationFailed(error)` matches
472
+ this exact response. The practitioner route can also return 403 when registration
473
+ is disabled, 409 `practitioner_registration_not_configured` when no default
474
+ policy is configured, 404 for an unknown tenant, 422 for invalid fields, and 429
475
+ when its five-per-minute limit is reached. `readOvokError(error)` exposes the
476
+ status/code/message and field errors for these responses.
477
+
478
+ ## Patient sharing and invitations
479
+
480
+ Patient sharing settings are read and written through `getPatientSharingSettings()`
481
+ and `updatePatientSharingSettings()`. The update always includes `enabled`; when
482
+ changing only practitioner sharing, send the current `enabled` value too.
483
+ `practitionerSharingEnabled` is a separate, off-by-default setting, not a
484
+ `ProjectSettingKey`. It controls new practitioner invitations and acceptances;
485
+ turning it off does not revoke existing shares. A project admin can toggle it
486
+ through `PUT /v1/patient-sharing/settings`; concurrent settings changes return
487
+ 409 and should be reloaded, and non-admins receive 403. Do not enable it for a project
488
+ whose practitioners use the Carehub dashboard. Map the email templates
489
+ `PATIENT_INVITE`, `PATIENT_ASSIGNMENT_REQUEST`,
490
+ `PRACTITIONER_INVITED_BY_PATIENT`, and `PRACTITIONER_PATIENT_SHARED` before
491
+ turning the feature on; an unmapped email can still produce the uniform 202
492
+ response without sending a message. `useUpdatePatientSharingSettings()` is the
493
+ corresponding mutation hook.
494
+
495
+ `listPatientSharing()` returns `outgoing`, `incoming`, and `practitioners`;
496
+ the latter lists active shares of a patient's whole record with practitioners.
497
+ For compatibility with older servers, the SDK defaults a missing `practitioners`
498
+ array to `[]`. Listing is patient-only (403 for another profile).
499
+ `revokePatientSharing(consentId)` uses the existing DELETE route
500
+ to end a share immediately or decline a pending invite. It returns 204; 404
501
+ (`Share not found.`) means the caller is not a party, and a code-less 409
502
+ (`This share kept changing; try again.`) can be retried after reloading. A
503
+ practitioner's whole-record share has no measurement-code list.
504
+
505
+ Invitations use four client methods, all backed by `/v1/invites/*`:
506
+
507
+ ```typescript
508
+ await client.invitePractitioner({ email, firstName, lastName }); // patient invites practitioner
509
+ await client.invitePatient({ email, firstName, lastName }); // practitioner invites patient
510
+ await client.acceptPractitionerInvite({ inviteId, code });
511
+ await client.acceptPatientInvite({ inviteId, code });
512
+
513
+ // Or dispatch acceptance using the signed-in profile:
514
+ await client.acceptInvite({ inviteId, code });
515
+ ```
516
+
517
+ `useInvitePractitioner()`, `useInvitePatient()`, and `useAcceptInvite()` provide
518
+ React mutation states. The invite methods always return `{ status: "sent" }`
519
+ (HTTP 202), whether or not an email is ultimately sent. Show only “If that
520
+ address can be invited, an email is on its way”; the response must not reveal
521
+ whether the address has an account. Both names are trimmed by Core, and email
522
+ addresses are normalized there. A new address receives an account and a
523
+ set-password link; an existing account receives an accept link. If sending the
524
+ email fails, Core rolls back the invite. Email is limited to 254 characters;
525
+ the trimmed first and last names must each be 1–200 characters.
526
+
527
+ The project must enable `PRACTITIONER_SHARING_ENABLED` and the invitee's
528
+ `PATIENT_INVITATION_ENABLED` or `PRACTITIONER_INVITATION_ENABLED` setting. An
529
+ invite can also fail with 401 (no session), 403 (feature/role gate), 422 (invalid
530
+ body), 409 `invitation_not_configured` (missing default policy),
531
+ `app_url_not_configured` (missing audience app URL), or
532
+ `practitioner_without_access_policy` (a full-access practitioner cannot be
533
+ narrowed to a share policy). A code-less 409 means another invite for the same
534
+ address or practitioner is still being written; offer retry. A 429 means the
535
+ inviter has 20 pending invites. Use
536
+ `readOvokError(error)` and `OVOK_INVITE_ERROR_CODES` to branch on stable codes.
537
+
538
+ ### Accept-invite page
539
+
540
+ Every app that can receive invitations needs a page at
541
+ `<app URL>/accept-invite?invite=<uuid>&code=<secret>`. Practitioner links use
542
+ `PRACTITIONER_APP_URL`; patient links use `PATIENT_APP_URL`. The secret is valid
543
+ for seven days. `parseAcceptInviteLink(url)` returns a body only when the UUID and
544
+ 16–128 character code are present and valid; otherwise show “This link is
545
+ incomplete” and make no request. Once parsed, remove the code from the address
546
+ bar/history and do not send it to logs or analytics.
547
+
548
+ If the visitor is signed out, send them through sign-in and restore the link on
549
+ return. There is no preview endpoint. Once signed in, call `acceptInvite()`; it
550
+ chooses the route from the `Patient` or `Practitioner` profile and rejects other
551
+ profile types. Never accept on page load. For a patient accepting, explain that
552
+ their whole record will be shared and wait for an explicit Accept action. A
553
+ patient can decline with `revokePatientSharing(inviteId)`; practitioners have no
554
+ decline route and can ignore the link. After a successful acceptance, show who
555
+ shares with whom (use a generic name when either is null), provide a way into the
556
+ app, and refresh `/auth/me` because practitioner access may have changed.
557
+
558
+ Render accept failures without attempting to distinguish private cases:
559
+
560
+ - 404 `Invite not found.` means an unknown id, bad code, or invite for another
561
+ account. Say the invite is not for the signed-in account and offer sign-out.
562
+ - 409 `This invite was already used.` means accepted or cancelled; the other
563
+ code-less 409 (`This invite kept changing; try again.`) can be retried.
564
+ - 410 means expired; ask the sender for a new invite.
565
+ - 409 `The patient who shared their record is no longer a member of this
566
+ project.`, `The practitioner who invited you is no longer a member of this
567
+ project.`, or `More than one account in this project acts as that practitioner.`
568
+ cannot be accepted; ask the sender to resolve it.
569
+ - 401 requires sign-in; 403 means the wrong account type or a disabled gate;
570
+ 422 means the link values are invalid.
571
+
355
572
  ## Large measurements
356
573
 
357
574
  `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>;