**Title:** `Annotated` handler parameters without explicit Litestar markers incorrectly override dependencies and path parameters

**Description:**
A handler parameter declared as `Annotated[<type>, <some constraint or metadata>]` (without a Litestar-specific marker like `Query`, `Path`, `Header`, etc.) is erroneously classified as a query parameter. This causes the parameter to be extracted from the query string at runtime, shadowing any identically‑named dependency or path parameter.

**Acceptance Criteria:**

1. **Dependency resolution takes precedence** – If a handler declares a parameter like `foo: Annotated[str, "some metadata"]` and a dependency named `foo` is provided, the dependency’s value is injected. No attempt to read `foo` from the query string is made, and no validation error about a missing query parameter occurs.

2. **Path parameters are not overridden** – For a route like `/{foo:int}`, a handler parameter `foo: Annotated[int, "some metadata"]` must be populated from the URL path segment. It must not be treated as a query parameter.

3. **Constraint metadata does not trigger query-param behavior** – Annotations that include constraint objects (e.g., from `annotated_types` like `Gt`, `Lt`) must not cause the parameter to be re‑classified as a query parameter. The parameter should behave as if it were declared with the plain underlying type and no `Annotated` wrapper, relying on existing dependency / path / body resolution rules.

4. **Explicit markers still work** – If an `Annotated` type *does* include a Litestar marker (e.g., `Annotated[str, Query(...)]`), that marker must continue to dictate the parameter source (query string, header, etc.) as documented.

5. **No regression for `name="data"`** – A parameter named `data` that uses `Annotated` should still be recognized as a body parameter and not be forced into a query-parameter role.

**Expected behavior in pseudocode:**
```python
@app.get("/{foo:int}", dependencies={"bar": provide_bar})
async def handler(
    foo: Annotated[int, "desc"],       # should come from path, not query
    bar: Annotated[str, "some meta"],  # should come from dependency, not query
) -> None:
    ...
```
Both `foo` and `bar` are resolved correctly according to their context, without any “missing required query parameter” errors.

fix(core): Fix `Annotated` parmeters implicitly treated as query parameters (cherry-picked from #4805) (#4806)


**Feature Request: Improved server push promise handling with ASGI extension awareness**

As a Litestar user, I want the `Request.send_push_promise()` method to clearly signal when the underlying ASGI server does not support the `http.response.push` extension, rather than silently doing nothing. I also want a way to query support at runtime, and more precise header forwarding when a push promise is sent.

### Acceptance Criteria

1. **Runtime support check**
   `Request` instances should expose a public, boolean attribute (e.g., `supports_push_promise`) that indicates whether the server’s ASGI scope includes the `http.response.push` extension. This must be populated from `scope["extensions"]` at connection init and never change.

2. **Backwards-compatible deprecation warning**
   When `send_push_promise(path)` is called and the server does *not* support push promises, the method must emit a `LitestarWarning` (deprecation category) and return without sending anything. The warning must state that a future version will raise an exception by default, and point to the new `raise_if_unavailable` parameter and the `supports_push_promise` attribute.

3. **Hard‑fail option**
   `send_push_promise()` must accept a new keyword‑only argument `raise_if_unavailable` (default `False`). When `True` and the server lacks the extension, a `LitestarException` is raised immediately. This allows projects to opt into the future behaviour early.

4. **Strict header inclusion**
   When constructing the push promise’s headers, only forward request headers that are *both* present in the incoming request and listed in the existing `SERVER_PUSH_HEADERS` set (e.g., `accept`, `accept-encoding`). Do **not** send empty value pairs for missing headers. This prevents sending unnecessary or malformed headers.

5. **Centralised ASGI extension keys**
   Introduce an enum (e.g., `ASGIExtension`) containing the standard ASGI extension identifiers (`http.response.push`, `http.response.zerocopysend`, `tls`, etc.) so that strings are not duplicated across the codebase.

### Constraints & Context

- The existing `scope["extensions"]` may be `None`; treat absence as an empty dict.
- The deprecation warning must be emitted even if `raise_if_unavailable` is `False`.
- The new attribute and the `raise_if_unavailable` parameter should be documented so users can write conditional code.

This change ensures that misconfigured servers are caught early (via warning or exception) while giving users full control over the severity, and fixes header forwarding to match the ASGI specification more faithfully.

feat: Improve ASGI extension interface (#2756)


The ASGI `scope["path_template"]` value is incorrect when multiple routes share a common prefix that includes a path parameter. For example, given a controller with two endpoints:

- `@get("/{id_:int}")` → path template `/prefix/{id_}`
- `@get("/{id_:int}/postfix")` → path template `/prefix/{id_}/postfix`

both endpoints currently return the same `scope["path_template"]` (the one belonging to the parent route) instead of their own distinct templates. This breaks any middleware, handler, or observability tool (like Prometheus metrics) that relies on `scope["path_template"]` to identify the matched route pattern.

**Acceptance criteria**

- A request to `/prefix/5` must set `scope["path_template"]` to exactly `/prefix/{id_}`.
- A request to `/prefix/5/postfix` must set `scope["path_template"]` to exactly `/prefix/{id_}/postfix`.
- This behaviour must hold for any combination of static segments and path parameters within a single route path, regardless of whether routes are registered via a controller or directly.
- Intermediate routing trie nodes (those representing only part of a route’s path) must not carry a full path template; only the node that handles the complete route may populate `scope["path_template"]`.

fix: set correct path_template value for trie node (#3806) (#3807)


As a user of the `medplum-provider` example app, I want to be able to confirm pending appointments that result from a hold workflow (e.g., after using the FooMedical `$find`/`$hold` flow).

**Acceptance criteria:**

1. **Pending-appointment styling on the calendar**
   - Appointments with status `"pending"` must appear visually distinct in the calendar widget. Instead of the standard filled background, they should show an outlined style (blue border, white background).
   - This distinction should not affect other appointment statuses (e.g., booked, cancelled).

2. **"Confirm Appointment" button in the appointment details drawer**
   - When viewing an appointment whose `status` is `"pending"`, a new button labeled **"Confirm Appointment"** must be visible in the details panel.
   - The button must **not** be rendered for any other status (booked, cancelled, etc.).

3. **Button behavior**
   - Clicking the button must send a `POST` request to the `$confirm` operation: `/Appointment/:id/$confirm`.
   - While the request is in flight the button should enter a loading state (disabled or spinner) and must not trigger duplicate requests.
   - On **success**:
     - The returned bundle may contain an updated Appointment (now status `"booked"`) and one or more updated Slot resources.
     - The appointment details must reflect the updated status, and the confirm button must disappear.
     - The main schedule page must update its slot state with the returned slots, so the calendar grid reflects any slot changes.
   - On **failure**, an error notification must be shown to the user.

4. **Symmetry with Cancel button**
   - The new confirm button should follow the same patterns as the existing "Cancel Visit" button in the same component: loading state handling, error notification, and search invalidation after success (`Appointment` and `Slot` searches).

5. **Updating the schedule page**
   - The schedule page that hosts the details drawer must be aware of slot updates resulting from a confirmation. When `$confirm` returns updated slots, those slots must replace the corresponding entries in the page’s slot state so the calendar immediately shows the correct slot availability.

**Context**
- This is only about the provider scheduling demo in `examples/medplum-provider`. The backend `$confirm` endpoint already exists; we only need the UI to invoke it and handle its response.

Implement the changes so that the above behaviors work end-to-end for a pending appointment.

Provider: Add Appointment confirm button (#9302)


```
Title: Batch conditional create does not reject resource with an explicit ID when fullUrl is missing or not a urn:uuid

Impact
---
Submitting a FHIR batch Bundle where an entry uses a conditional create-by-update (PUT to a search URL, e.g. `Patient?_id=...`) and the resource body carries an id field should always be rejected with a 400 status and the message `Cannot provide ID for create by update`. Currently Medplum silently accepts such requests when the entry does *not* have a `fullUrl` starting with `urn:uuid`. This allows client-assigned IDs to leak into conditional creates, potentially creating server-assigned-ID resources with a pre-populated `id` and causing data integrity or interoperability problems.

Expected behaviour
---
All batch entry identity-resolution and validation logic – including the check that rejects an explicit `id` on a conditional create – must execute for every entry, regardless of whether its `fullUrl` is a `urn:uuid` placeholder. Entries without a `fullUrl` or with a different fullUrl scheme (e.g. no fullUrl at all) should still be processed through the same identity/validation step.

Specifically, when processing a batch Bundle entry whose request method is `PUT` and whose URL contains query parameters (indicating a conditional operation), and the supplied resource has an `id` field set, the server must respond with a `400` status and an OperationOutcome indicating `Cannot provide ID for create by update`.

Current workaround
---
The bug only appears for entries that lack a `urn:uuid` fullUrl; clients that always attach such a fullUrl are not affected.

Request
---
Ensure the batch processor’s identity-resolution logic (the part that enforces the “no ID in conditional create” rule, and any related fullUrl‑based identity assignments) is applied to *all* bundle entries, not just those whose `fullUrl` starts with `urn:uuid`. This will make the validation consistent across every entry type and restore compliance with FHIR conditional-create semantics.
```

Ensure identity resolution logic covers all batch operations (#7614)


**Title:** Add CCDA validation button to `CcdaDisplay` component

**Background:** The `CcdaDisplay` component (packages/react) shows a C‑CDA document from a given URL inside an iframe. We need to let users validate the CCDA document using the Medplum CCDA validator service without leaving the viewer.

**Request:**

Add a “Validate” button below the iframe. When clicked, the component should:

1. Fetch the raw CCDA XML from the provided `url`.
2. POST that XML (as a multipart form field named `ccdaFile`) to the Medplum CCDA validator endpoint:
   `https://ccda-validator.medplum.com/referenceccdaservice/?validationObjective=C-CDA_IG_Plus_Vocab&referenceFileName=No%20scenario%20File&curesUpdate=true&severityLevel=WARNING`
3. The POST must work cross-origin: use `credentials: 'omit'` and `redirect: 'manual'`.
4. Parse the JSON response and display a summary line showing the number of errors found: count of entries in `resultsMetaData.resultMetaData` whose `type` contains the string `"Error"`.
5. If the call fails (non‑2xx HTTP status), log the error to the console and show nothing — the Validate button returns to its normal enabled state.
6. While the request is in flight, disable the button and show “Validating…”.
7. When validation results are available, also show a “Download Full Results” button that exports the whole response object as a JSON file.

**Download helper availability:**
The `exportJsonFile` utility currently lives only in the app package. The `CcdaDisplay` component belongs to the `@medplum/react` package and must not import from the app. Move `exportJsonFile` into the react package’s utils (e.g., `packages/react/src/utils/dom.ts`) so it can be used directly from there. Update the app package to import it from `@medplum/react`.

**Service‑error handling:**
If the validator returns `resultsMetaData.serviceError: true`, still show the error count (which will be 0 because `resultMetaData` is empty) rather than treating it as a failure.

**No‑URL case:**
If no `url` prop is supplied, the component renders nothing — the Validate button should not appear.

feat(react): add CCDA validation to `CcdaDisplay` (#6203)


Implement the FHIR `$apply` operation on `ChargeItemDefinition`. The operation must accept a `Parameters` input with a `chargeItem` parameter whose value is a reference to an existing `ChargeItem`. The handler should:
- Read the `ChargeItemDefinition` from the request path.
- Read the `ChargeItem` referenced in the input parameter.
- Add the `ChargeItemDefinition`'s canonical reference to the charge item’s `definitionCanonical` array, without duplicating it if already present.
- If the `ChargeItemDefinition.propertyGroup` contains a price component with type `“base”`, set the charge item’s `priceOverride` to that amount.
- Persist the updated charge item and return it (status 200).

The implementation must use the same authentication context and parameter parsing helpers as the existing `PlanDefinition/$apply` handler, and the new route must be registered alongside it.

Add operation chargeitemdefinition $apply (#6194)


**Feature request: debounce `useSearch` / `useSearchOne` / `useSearchResources`**

The `useSearch`, `useSearchOne`, and `useSearchResources` hooks currently fire a FHIR search request on every query-parameter change.  When the query parameters update rapidly (e.g. a user typing in a search field), this produces a storm of requests.  We want to debounce those requests so only the last query in a rapid burst is actually sent.

Requirements / invariants:

- The debounce delay should be configurable via an options parameter on each hook (e.g. `{ debounceMs: <number> }`).
- The first search when the hook mounts (or when a new value arrives after a period of inactivity) should fire **immediately** (leading edge), so there’s no initial lag.
- Subsequent rapid changes within the debounce window should be collapsed: intermediate searches are skipped; only the final search after the window expires is executed.
- Use a sensible default debounce period (e.g. 250 ms) when no `debounceMs` is supplied.
- The debounce behaviour must work the same way across `useSearch`, `useSearchOne`, and `useSearchResources`.
- The hook’s public contract (return type) must not change; the existing `[result, loading, outcome]` tuple must remain compatible.

feat(useSearch): add debounce to `useSearch` (#5413)


The server's JSON body parser currently only activates for a very narrow allowlist of Content-Types. When a client sends a request with a JSON body but uses a standard `application/*+json` media type that isn't in that list (e.g. `application/ld+json`, `application/vnd.api+json`, `application/cloudevents-batch+json`), the request body is left unparsed and downstream handlers see `req.body` as undefined or empty. This breaks integrations that follow the `+json` structured syntax suffix convention (RFC 6839).

We need to accept any `application/*+json` Content-Type while still applying the existing `maxJsonSize` limit. The parser must still treat legacy non-application types like `text/json` and `text/x-json` as non‑JSON, and invalid types like `json/application` must also be rejected – only well-known `application/…+json` types and plain `application/json` should trigger JSON parsing.

**Acceptance criteria**
- For any Content-Type matching `application/*+json` (including `application/fhir+json`, `application/json-patch+json`, `application/scim+json` etc.) the request body is parsed as JSON.
- `application/json` continues to work as before.
- `text/json`, `text/x-json`, and other non‑`application/` types are **not** parsed as JSON.
- The JSON body size limit (`maxJsonSize`) remains enforced for all accepted types.


**Title:** FHIRcast hub publishes duplicate heartbeats when multiple clients subscribe to the same topic

**Context**
Our FHIRcast WebSocket handler (`packages/server/src/fhircast/websocket.ts`) uses the global heartbeat module (`../heartbeat`) to send periodic heartbeat events to connected clients. Each heartbeat tick should result in exactly one heartbeat message per active topic, as required by the FHIRcast specification.

**Problem**
Currently, every new WebSocket connection attaches its own listener to the heartbeat event emitter inside `handleFhircastConnection`. When multiple clients are subscribed to the same topic, the server publishes one heartbeat message per connected client on that topic, instead of a single heartbeat per interval. Additionally, the listeners are never cleaned up when a client disconnects unless the client itself closes—but the per-connection listener model also makes it difficult to stop heartbeats for a topic once all subscribers have left.

This causes:
- Duplicate heartbeat messages on the wire, violating the spec and potentially confusing clients.
- Unbounded growth of listeners and wasted CPU/memory as connections come and go.

**Desired behavior**
The FHIRcast subsystem should maintain at most **one** global heartbeat listener, regardless of how many WebSocket connections exist. That single listener should iterate over all **currently active topics** (topics that have at least one subscribed WebSocket) and publish exactly one heartbeat per topic per tick.

When the last WebSocket for a given topic disconnects, the topic should be removed from the active set so that heartbeats stop for that topic. Cleanup must not rely on removing per-connection listeners; instead, the global handler and its lifecycle should be managed independently from individual connections.

**Additional ask**
While refactoring, please emit OTel gauges to track the number of active WebSocket connections and the number of active FHIRcast topics. This will help us monitor the subsystem’s health.

**Constraints / Edge cases**
- The global handler must be idempotent (registering it multiple times from different connection handshakes should not create duplicate listeners).
- Reference counting per topic must handle concurrent connection open/close correctly (Node.js is single-threaded, but async interleaving is possible).
- If a client connects, subscribes, and immediately disconnects before the next heartbeat tick, the topic should be removed and no heartbeat published.
- The Redis channel used for publishing must remain consistent with the existing `project:topic` format. The `hub.topic` field in the heartbeat payload must still reflect the FHIRcast topic (i.e., the part after the colon).

fix(fhircast): make sure only one heartbeat listener for all of FHIRcast (#5807)


**Title:** ThreadInbox: allow locking the patient field to the current user for patient-facing apps

**Background**
The `ThreadInbox` component and its “New Topic” dialog (`NewTopicDialog`) always show an editable patient search input, even when the current user is a patient and a `subject` is already provided. In patient-facing applications (e.g., `foomedical`), the patient should not be able to change the patient — it must be set to themselves and locked.

**Request**
Add a new optional boolean prop `allowPatientSelection` to `ThreadInbox`, with a default of `false`. The prop should be forwarded to `NewTopicDialog` to control the behavior of the patient field in the new-message dialog:

- When `allowPatientSelection` is `true` (provider-facing apps), the patient field remains an editable search input, and the hint “Select a patient” is shown below the label.
- When `allowPatientSelection` is `false` (the default, or explicitly patient-facing):
  - If a `subject` (patient) is provided, the patient field is pre-filled and **disabled**. The hint text is **not** shown.
  - If no `subject` is provided, the field remains editable (as today) so the user can still choose a patient.

This keeps existing behavior unchanged when no `subject` is set, and gives patient apps a way to lock the field without breaking provider workflows.

**Concrete steps**

1. Extend both `ThreadInboxProps` and `NewTopicDialogProps` with `allowPatientSelection?: boolean`.
2. Default the prop to `false` in both components.
3. In `NewTopicDialog`, conditionally render the “Select a patient” hint only when `allowPatientSelection` is `true`. Set the `disabled` prop on the `ResourceInput` for the patient to `true` when `!allowPatientSelection && !!patient` (i.e., locked because we have a pre-filled subject).
4. Pass the prop through from `ThreadInbox` to `NewTopicDialog`.
5. Update the example applications:
   - `foomedical` (`MessagesPage`): set the `subject` prop to the current user’s profile when it is a `Patient`. Leave `allowPatientSelection` at its default `false` so the patient is locked.
   - `medplum-provider` (`MessagesPage`): set `allowPatientSelection={true}` to keep the existing provider experience where a patient can be searched and selected.

**Acceptance criteria / behavior to verify**

- In a patient-facing app where the logged-in user is a `Patient` and `subject` is the user’s own reference, the “New Message” dialog shows a disabled patient field pre-filled with the user’s name, and no “Select a patient” hint.
- In a provider-facing app with `allowPatientSelection={true}`, the dialog shows an empty, editable “Patient” field with a “Select a patient” hint, and validation requires a patient to be chosen.
- When `allowPatientSelection` is `false` but no `subject` is supplied, the patient field still behaves as a required editable search input (existing behavior preserved).
- No regressions in the existing chat/messaging experience.

feat(react): add allowPatientSelection prop to ThreadInbox for client facing apps (#8563)


**Title:** Add cron queue gauges and move recurring metrics to periodic heartbeat

**Description:**
Currently, several OpenTelemetry gauges (`medplum.db.idleConnections`, `medplum.db.queriesAwaitingClient`, `medplum.node.usedHeapSize`, `medplum.subscription.waitingCount`, etc.) are emitted only when the `/healthcheck` endpoint is called. This couples gauge freshness to external polling and misses times when healthchecks aren't invoked. We want these gauges to be recorded continuously at regular intervals, independent of healthcheck calls.

Additionally, add analogous metrics for the cron BullMQ queue: `medplum.cron.waitingCount` and `medplum.cron.delayedCount`, sourced from the cron queue in the same way subscription queue metrics are sourced.

**Behavioral requirements:**

- All recurring gauges (DB pool idle/awaiting, Node heap sizes, subscription queue lengths, and the new cron queue lengths) should be recorded on each internal heartbeat tick rather than in the healthcheck handler.
- The healthcheck handler must **continue** to record its own round-trip-time/status gauges (e.g., `medplum.db.healthcheckRTT`, `medplum.redis.healthcheckRTT`), but should no longer emit the periodic gauges listed above.
- Registration of the heartbeat listener must be **idempotent** (calling initialization multiple times must not create duplicate listeners).
- Cleanup during graceful shutdown must remove the listener to avoid leaks.

**Constraints:**

- Use the existing `setGauge` helper for all metrics.
- Use the existing internal heartbeat event mechanism.
- Preserve existing attributes (e.g., `hostname`, `dbInstanceType: 'writer'`/`'reader'`) on the DB gauges.

The goal is to close the monitoring gap for the cron queue and make gauge emission independent of external healthcheck polling.

metrics(bullmq): add metric for cron queue length (#5829)


Please add the user's email address to the JWT payload for access tokens and ID tokens when the `email` scope is present in the authorized scopes and the authenticated principal is a `User` resource.

Requirements:
- When the `email` scope is included in the login scope and the `user` resource is a `User` with a non-undefined `email` property, include `email` in the claims of both the access token and the ID token.
- If the principal is a `ClientApplication`, omit `email` from both token types even if the `email` scope was requested.
- If the `User` does not have an `email` value, omit the claim.
- Only add the `email` field to the access token claims interface; ID token claims should also accept and include it (the field already exists or should be added to the ID token claims interface as needed).
- The behavior should be reflected in both token generation functions used during OAuth flows (`generateAccessToken` and `generateIdToken`), and in the `getAuthTokens` utility that constructs the claims from the `Login` and `User` resources.


```
Make the encounter Billing tab fall back to the encounter's own FHIR ID for
Candid lookups when the stored Candid encounter identifier is missing.
Some encounters never receive a persistent Candid ID, but the encounter's
ID can be used as an external key to retrieve claim status.

Also improve the claim submission flow:
- Cancel any pending debounced auto-save of the claim before submitting
  to avoid silently overwriting the submitted state with stale data.
- After a successful submission, immediately refresh the Candid encounter
  status so the panel reflects the latest status without a page reload.
```

fix(provider): Use external id in Candid when Candid encounter id is missing (#8591)


Below is a terse implementation request that would plausibly lead to the shown patch.

---

Title: Mask secret string fields and add reveal/copy controls in the resource property display

Currently, all string values in the resource property display rendering are shown in plain text. For any string-typed property whose path contains `secret` (case‑insensitive), the value must be masked by default.

Acceptance criteria:

- When a string property path includes the substring `secret` anywhere (e.g., `ClientApplication.clientSecret`, `Device.credentials.secret`, or just `secret`), display a fixed mask (e.g., eight bullet characters) instead of the real value.
- Provide two small action buttons next to the masked value (both should be absent when the field value is empty or null):
  1. **Copy** – copies the actual secret value to the clipboard. Follow the same pattern used for the existing ID copy button (tooltip, subtle `ActionIcon`, toggling icon on successful copy).
  2. **Show/Hide** – toggles between masked view and the real value, with an appropriate eye/eye‑off icon and tooltip.
- When the value is empty string or `null`, no buttons or mask rendering should appear (just nothing).
- When revealed, the real value must use `whiteSpace: 'pre-wrap'` to preserve any formatting, consistent with how regular strings are displayed.
- Regular (non‑secret) string fields must remain completely unchanged.

The feature should only affect the display layer; no changes to data or API behavior are needed.

Mask secret fields of type string (#7301)


**Title:** Lookup-table deletions attempt to remove rows for resource types that never have those properties

**Severity:** Medium (unnecessary database work; potential for errors when references are missing)

**Description:**

When a resource is deleted (or purged), the system calls `deleteValuesForResource` / `purgeValuesBefore` on lookup tables such as `AddressTable` and `HumanNameTable` regardless of the resource type. Many resource types never contain an `address` or `humanName` field, so these tables will never hold rows for those types. Attempting to delete rows from these tables for such resources generates pointless SQL statements and, in some environments, could trigger errors if the query planner reacts poorly to tables that lack the expected rows or statistics.

**Expected behavior:**

- `AddressTable.deleteValuesForResource` and `AddressTable.purgeValuesBefore` should check whether the resource type is one that can have addresses. The class already has a static `hasAddress` helper that answers this; use it as a guard before issuing any delete.
- `HumanNameTable.deleteValuesForResource` and `HumanNameTable.purgeValuesBefore` should apply the same pattern. Determine the set of resource types that can carry a human name (Patient, Person, Practitioner, RelatedPerson) and skip the delete entirely when the resource type is not in that set.
- The guard should be applied at the beginning of each operation so that no query is executed for irrelevant resource types.

**Non-obvious edge cases / safety constraints:**

- The check must use the same resource-type classification that the corresponding index-insertion path uses (i.e., a resource type that will never have rows in the lookup table should not trigger an attempt to remove rows).
- The `purgeValuesBefore` operation needs to join the main resource table; the guard should prevent that join from being built when the lookup table could not possibly contain matching rows for the given type.
- Both direct deletion of a single resource and bulk purge by type must be covered.

**Impact:** Eliminates wasted database work and removes risk of confusing errors when deleting “wrong-type” resources.

Skip lookup table deletions for superfluous resource types (#5946)


Add a "Remove user" button to the MemberDetailsPage that lets project admins remove a member from the project.

- The button must appear only for admins (`project admin === true`) and must never appear for the project owner (the owner cannot remove themselves).
- Clicking the button shows a confirmation dialog; if confirmed, call the delete endpoint for the membership (`admin/projects/{projectId}/members/{membershipId}`).
- After a successful deletion, invalidate all ProjectMembership search caches and navigate back to the member list (the correct list page – users, bots, or clients – depending on which admin section the page was opened from).
- If the deletion fails, show an error notification with the error message.


The bot execution endpoints (`/fhir/R4/Bot/$execute`, CDS hooks, and webhook) currently contain separate, slightly different logic for sending bot results back to the client. This causes two problems:

1. **Inconsistent content types** – When a bot returns a FHIR resource, the response sometimes uses `application/json` instead of `application/fhir+json`.
2. **Crash on non-Binary FHIR resources in unauthenticated contexts** – Webhook and CDS endpoints can fail if the bot returns a FHIR resource (other than Binary) because `sendFhirResponse` unconditionally calls `getAuthenticatedContext()` to rewrite attachment URLs, and those contexts may not be authenticated.

Unify the response sending so that all bot execution paths share a single, correct implementation. The unified behavior must:

- Send an `OperationOutcome` directly (with proper HTTP status) when the bot result itself is an `OperationOutcome`.
- Wrap an undefined return value in an `OperationOutcome` with the log result.
- For FHIR resources (any resource type), route through `sendFhirResponse` so the content type is always `application/fhir+json` and attachment rewriting happens when available.
- For non-FHIR return values, mirror the request content type for `text/*` and HL7v2 content types, and set the HTTP status according to success/failure.

Additionally, fix `sendFhirResponse` so that it no longer assumes an authenticated request context is always available. When no `AuthenticatedRequestContext` is present, skip attachment rewriting instead of throwing.


**Feature Request: Allow PlanDefinition workflows to set task owner and performer type**

When applying a PlanDefinition that references an ActivityDefinition (e.g., generating a ServiceRequest and Task), the resulting Task’s `owner` is currently always the patient (subject), and `performerType` is never populated. We need the ability for plan authors to configure the task owner dynamically and assign a performer type via a Medplum extension on the ActivityDefinition.

### Desired Behavior

- The `PlanDefinition/$apply` operation should accept two new optional parameters:
  - `practitioner` – a reference to a Practitioner
  - `organization` – a reference to an Organization
- When the ActivityDefinition in the plan includes the extension with URL `https://medplum.com/fhir/StructureDefinition/task-elements`:
  - The `owner` sub-extension must be a `valueExpression` containing a FHIRPath expression that can reference `%practitioner`, `%organization`, and `%subject`. Evaluate this expression to obtain a FHIR reference that will be set as the Task’s `owner`.
  - The `performerType` sub-extension must be a `valueCodeableConcept`. Its value should be placed into the Task’s `performerType` array (as a single-element list).
- If the ActivityDefinition does **not** have the `task-elements` extension, the behavior must remain unchanged: the Task’s `owner` still defaults to the subject (patient), and no `performerType` is set.
- The ServiceRequest created alongside should continue to be linked normally (subject, encounter, etc.), and any existing workflow generation paths (questionnaire tasks, direct tasks) must not be affected.

### Constraints & Edge Cases

- The new parameters are optional; omitting them should not break existing $apply calls.
- The FHIRPath evaluation for `owner` must be scoped to the provided references – if the expression yields a value of type Reference, use it; otherwise, do not set an owner (respect the default if extension is absent).
- The `task-elements` extension should only affect tasks created from ActivityDefinitions; other definition types (Questionnaires, plain tasks) should ignore it.
- Maintain backward compatibility: all existing tests and usages of PlanDefinition apply with only `subject` and `encounter` should keep producing Task with `owner = subject`.

### Implementation Notes (for context)

The `task-elements` extension is already defined within Medplum’s FHIR structure definitions. The values used in expressions are the actual FHIR references, not string representations, so the implementation should evaluate against typed values (the Practitioner/Organization/Patient resources).

Support performerType and owner in PlanDefinition (#7202)


**Title:** Agent upgrade should be no-op when already on target version, and support a `force` flag

**Background**
When a user triggers an agent upgrade (via `Agent/$upgrade` or directly via websocket message) to the same version the agent is already running, the agent currently goes through the full download‑and‑restart process. This is wasteful and can lead to unnecessary downtime or failures. The agent should recognise that it is already on the requested version and respond with success immediately, without performing any upgrade steps.

Additionally, there are legitimate reasons to force a re‑install of the same version (e.g., when a manually installed pre‑release build needs to be replaced with the corresponding release build). The agent should therefore support a `force` flag that, when present, causes the upgrade to proceed even if the version matches.

The server‑side `Agent/$upgrade` operation handler currently parses its input parameters manually, which produces inconsistent error messages. While adding the new `force` parameter, the handler should also be aligned with the standard parameter‑parsing approach used by other FHIR operations so that error messages include the parameter name.

**Acceptance criteria**

1. **Agent no‑op behaviour**
   - When the agent receives an `agent:upgrade:request`, determine the target version (from the `version` field, or fetch the latest release if absent).
   - If the current `MEDPLUM_VERSION` (the part before any `-` suffix, i.e. the `major.minor.patch` prefix) starts with the target version string, and the request does **not** include `force: true`:
     - Log an informative message indicating the agent is already on that version.
     - Send back an `agent:upgrade:response` with status 200 and the appropriate callback.
     - Do **not** download or restart anything.
   - If `force: true` is present and the version matches, log a message stating that the upgrade is being forced and then proceed with the normal upgrade flow (download, replace, restart).

2. **Agent request shape**
   - Add an optional boolean field `force` to the `AgentUpgradeRequest` interface in `packages/core/src/agent.ts`. Do not change existing fields or response types.

3. **Server `$upgrade` operation**
   - Add an `in` parameter `force` (type `boolean`, cardinality 0..1) to the operation definition in `packages/server/src/fhir/operations/agentupgrade.ts`.
   - Update the handler to extract this parameter and forward it as the `force` field on the `agent:upgrade:request` message sent to the agent.
   - The handler should parse all input parameters (version, timeout, force) using the same parameter‑parsing infrastructure already used by other operations (found under `packages/server/src/fhir/operations/utils/parameters`). This will also yield more precise error messages that include the parameter name (e.g. `Invalid value '...' provided for integer parameter 'timeout'` instead of the current generic message).

4. **Tests**
   - Verify that an upgrade request to the agent’s current version (without force) results in a successful no‑op response and does not spawn any upgrade child process.
   - Verify that the same request with `force: true` does trigger a full upgrade (download, child process, etc.).
   - Verify that the server `$upgrade` operation handles the `force` query parameter and passes it to the agent correctly, and that the handler uses the standard parameter parsing.

**Notes**
- `MEDPLUM_VERSION` follows the pattern `x.y.z-<commit_hash>`; the agent should strip the commit hash when comparing versions.
- The `force` flag should be optional on both the agent message and the FHIR parameter to preserve backward compatibility.

fix(agent): upgrade is no-op when already on target version (#6357)


```
Title: Reindex job does not reset pagination cursor when advancing to the next resource type

When a reindex job processes a list of resource types (e.g., ["Patient", "Observation"]),
the worker paginates through each type using a `cursor`. After one type finishes, the worker
enqueues a follow-up job for the remaining types. That follow-up job currently carries over
the `cursor` from the previous type, so the next resource type starts with an offset that
does not belong to it. This can cause resources of the later type to be skipped entirely.

Expected behavior: each resource type should begin reindexing from the first page
(cursor must be set to `undefined`).

Fix: when `execReindexJob` enqueues a new job for the next resource type, ensure that the
job data includes `cursor: undefined` together with the existing `count: 0` reset.

Affected code: the reindex worker (`packages/server/src/workers/reindex.ts`).
```

Reset reindex cursor between resource types (#5116)


**Title:** Duplicate user creation via SCIM returns 400 instead of 409 Conflict

**Description:**
When a SCIM client (e.g., Okta) sends a POST to `/scim/v2/Users` for a user that already exists in the project, the endpoint returns HTTP 400 Bad Request with a message like “User is already a member of this project”. This violates the expected SCIM behavior defined in RFC 7644 §3.3, which specifies that a duplicate resource creation should result in a 409 Conflict. The integration with Okta fails because Okta treats 400 as a client error and does not recover, whereas it handles 409 gracefully (e.g., by linking the existing user).

The same error semantics appear in the admin invite endpoint (POST `/admin/projects/{projectId}/invite`) when attempting to re-invite a member; it also returns 400 instead of 409.

**Expected behavior:**
- The SCIM user creation endpoint should return HTTP 409 Conflict when the user is already a member of the project, with a SCIM-compliant error body.
- The admin invite endpoint should also return 409 Conflict for the same logical conflict.
- Username lookups via the SCIM filter `userName eq "..."` should be case-insensitive, as required by the SCIM specification. (Please verify and ensure this is working, adding necessary test coverage.)

**Impact:**
- Okta provisioning fails for existing users.
- RESTful clients cannot distinguish a genuinely malformed request from a resource conflict, making automated integrations brittle.

**Acceptance criteria:**
- Duplicate user creation via SCIM returns 409 Conflict.
- Duplicate admin invite returns 409 Conflict.
- SCIM username filtering is case-insensitive; searching for an existing user with a different case returns that user.


Fix the GraphQL max depth validation so it uses actual field nesting depth rather than counting AST path segments. The current metric miscounts depth when queries use fragment spreads or inline fragments, causing false positives and inconsistent limits. Depth should track the number of nested field selections, ignoring fragment boundaries. Additionally, exceeding the depth limit should be downgraded from a hard validation error to a logged warning only.

Fix GraphQL max depth validation (#4949)


**Title:** Add priority filter to Task Board in Provider example

**Description**

The Task Board in the Medplum Provider example currently lets users filter tasks by status and performer type through the filter menu. Users need to be able to filter tasks by priority as well, so they can quickly find urgent or routine tasks.

**Acceptance Criteria**

1. **Priority submenu in the filter menu**
   - The `TaskFilterMenu` component should show a new “Priority” option, similar to the existing “Status” submenu.
   - The submenu lists the standard FHIR Task priority values: `routine`, `urgent`, `asap`, `stat`.

2. **Multi-select toggle behavior**
   - Users can select one or more priorities at the same time. When a priority is selected, the board filters tasks to show only tasks with that priority.
   - Clicking a priority again removes it from the selection. If all priorities are deselected, the filter is cleared and tasks of any priority are shown.
   - Currently selected priorities should have a visible indicator (e.g., a checkmark) in the submenu.

3. **URL query string integration**
   - The selected priorities must be persisted in the URL query string (e.g., `?priority=urgent,stat`).
   - Navigating to a URL that contains a priority query parameter should initialize the filter menu with those priorities selected and show the matching tasks.
   - When the user changes priority selections, the URL updates accordingly.

4. **Combination with other filters**
   - The priority filter must work together with existing status filters. Applying both a status filter and a priority filter should show only tasks matching **all** selected criteria.
   - Removing either filter should not affect the other.

5. **API interaction**
   - When priority filters are active, the FHIR `Task` search request must include the `priority` parameter with the selected values as a comma-separated list (matching the existing pattern for status values in the API call).

**Expected behavior summary**
The priority filter should behave exactly like the existing status filter in terms of UI interaction, URL state management, and search query construction. Adding this filter should not break any existing filtering capabilities.

Provider - Tasks include priority (#8152)


**Title:** Improve type safety in FHIR operation parameter utilities

### Background
The server’s FHIR operation parameter helpers (used for parsing input parameters and building output `Parameters` resources) rely heavily on `any` annotations, which bypass TypeScript checks and can hide bugs.

### Acceptance criteria

1. **Eliminate `any` from the parameter utilities**
   In the `parameters.ts` module under the `server` package, replace all explicit `any` uses with `unknown` for function parameters, return types, and local variables where the exact shape is not known. The public generic functions (`parseParameters`, `parseInputParameters`) must continue to return type `T` and must not use `as any` casts.

2. **Guard string-typed query parameters**
   The helper that parses a single query-string value (`parseStringifiedParameter`) should accept `unknown` instead of `string` and return `undefined` when the input is not a string. This prevents runtime crashes when a query parameter is not a plain string (for example, an array from `qs` or `URLSearchParams`).

3. **Safer output building**
   `buildOutputParameters` should accept its `output` argument as `object | undefined` (not `any`). Property access on the output object must be preceded by a cast to `Record<string, unknown>` only after confirming the object is defined.

4. **Safer handling of nested parts and typed values**
   `makeParameter` must accept `unknown` instead of `any`. When processing nested parameter parts (`param.part`), it should first verify that the value is an object. The inline check that detects a `TypedValue` (looking for `type` and `value` properties) must be replaced with a call to a dedicated, reusable type guard.

5. **New `TypedValue` type guard in `@medplum/core`**
   Add and export a type guard function from `packages/core/src/types.ts` that determines whether a value of type `unknown` conforms to the `TypedValue` interface: an object that has both a `type` property of type `string` and a `value` property. This guard should narrow the value to `TypedValue`.

6. **Preserve runtime behaviour**
   All existing unit tests must pass without modification. The only intentional behavioural change is the stricter string check in query-parameter parsing; for every other valid input the output must remain identical.

Removed 'any' from fhir operation param utils (#6206)


```
Title: Authentication errors should be logged and return a proper OperationOutcome

Currently, when the server encounters an authentication error during request processing (for example, when a valid token references a deleted ProjectMembership), the request is not recorded in the request logs and the error response varies (often a raw 403 Forbidden without a structured OperationOutcome). This makes client-side error handling unpredictable and complicates server-side debugging.

Expected behavior:
- Requests that fail authentication (including token validation errors, missing or deleted memberships, insufficient scopes detected during authentication) must still appear in the request logs with the same fields as successful or other failed requests (method, path, status, duration, etc.).
- The HTTP status code for authentication failures should be 400 Bad Request.
- The response body must be a FHIR OperationOutcome with at least one issue where `details.text` is "Authentication error" and `diagnostics` includes the original error information (such as the root cause exception type and message).
- The authentication failure must also be emitted as an error-level log entry on the server, containing the error message and stack for diagnostic purposes.
- The fix must not affect the logging or response behavior for requests that succeed or fail for other reasons (e.g., validation errors, business logic rejections).
- The request log should be emitted even if the authentication middleware itself cannot fully set up a request context, as long as the HTTP request was received and a response is sent.
```

Log authentication middleware errors (#8415)


Fix two bugs in the `SearchControl` component:

1. The `total` value from the current search request is not forwarded to the API call, so the total count is missing from the response and the UI shows no total. Include `total` in the query parameters, defaulting to `"accurate"` when no value is provided.
2. When the result set is empty, the count display shows an invalid range (e.g., "0– -1 of 0"). Ensure the end of the range is never negative: for an empty result, the range should be "0–0 of 0".

Include `total` in `SearchControl` (#4253)


**Title:** Add mutable setter methods to `Hl7Message`, `Hl7Segment`, and `Hl7Field`

**Background**
Our HL7 classes currently provide rich getter semantics (`getField`, `getComponent`, `get`) but no way to *mutate* an already-parsed HL7 message. Users who need to modify a message (e.g., to adjust a patient ID, update a timestamp, or remap segments) are forced to rebuild the whole structure manually. We should add setter methods that mirror the existing getter shape, respect HL7 indexing conventions, and enforce the special invariants of the MSH header.

**Requested API (public surface)**

1. **`Hl7Message.setSegment(index: number | string, segment: Hl7Segment): boolean`**
   - Replace a segment at a given index (0-based numeric) or by name (first match).
   - **MSH rules:**
     - Only an MSH segment can be placed at index 0 (i.e., you cannot replace the MSH with a non-MSH segment).
     - An MSH segment can only be placed at index 0 (attempting to set MSH at a different index or replace a non-MSH segment by name with MSH returns `false`).
   - If a numeric index exceeds the current segment count, append the new segment at the end.
   - Return `true` on success, `false` if constraints are violated or a named segment doesn’t exist.

2. **`Hl7Segment.setField(index: number, field: Hl7Field | string): boolean`**
   - Set a field at a 1-based index (consistent with HL7 numbering). If the index is greater than the current number of fields, the field array should be extended before setting.
   - **MSH special handling:**
     - MSH.1 (field separator) and MSH.2 (encoding characters) MUST NOT be changed; attempts to set field 1 or 2 on an MSH segment must return `false`.
     - For MSH segments, field indices 3+ are stored with an offset (field 3 is the first field after the separator/encoding characters, matching how `getField` works). Setting MSH field 3 replaces that first logical field while preserving MSH.1 and MSH.2.
   - Accept a string as a convenience, parsing it into an `Hl7Field` before storing.

3. **`Hl7Segment.setComponent(fieldIndex: number, component: number, value: string, subcomponent?: number, repetition?: number): boolean`**
   - Shortcut to set a component within a field.
   - `fieldIndex` and `component` are 1-based; `repetition` defaults to 0.
   - Return `false` if the field does not exist at `fieldIndex`.
   - Delegate to `Hl7Field.setComponent` for the actual storage.

4. **`Hl7Field.setComponent(component: number, value: string, subcomponent?: number, repetition?: number): boolean`**
   - Set a component at the given 1-based `component` index within a repetition (default repetition 0).
   - If `subcomponent` is provided, treat the target as a subcomponent: split the existing component value by the context’s subcomponent separator, modify that subcomponent index, and rejoin.
   - Extend internal structures as needed: e.g., if a repetition doesn’t exist, add it; if a subcomponent array isn’t long enough, pad with empty strings.
   - Return `true` on success, `false` if component < 1 or subcomponent < 0.

**Behavioral invariants / edge cases**
- All indices that correspond to HL7 field/component numbers are **1-based**, mirroring the existing getter pattern. Repetition and segment indices are **0-based** (segment index matching array position).
- Extending collections (fields, repetitions, components) when an index is out of bounds should always succeed, filling intermediate slots with empty values where necessary.
- MSH.1 (`|`) and MSH.2 (`^~\\&` etc.) are immutable across all setters on MSH segments.
- The setter must respect the existing separation between MSH’s internal representation (fields 0–n) and the logical HL7 numbering that `getField`/`getComponent` expose. The patch should not break existing getter behavior.
- No setter should throw; use `false` returns to indicate invalid operations.

**Additional fix (low priority, can be bundled)**
- The JSDoc on the deprecated `Hl7Segment.get(index)` incorrectly advises using `getSegment()`; it should advise `getField()`. This is a one-line doc comment change. Include it if you touch that class.

Implement Set Functions for HL7 Messages (#6627)


The OpenTelemetry heartbeat listener currently publishes gauge metrics for the `subscription` and `cron` queues only (`medplum.subscription.waitingCount` / `delayedCount`, `medplum.cron.waitingCount` / `delayedCount`). We need equivalent visibility into the `batch` and `download` queues.

**Acceptance Criteria**

1. On every server heartbeat, the following additional metrics are emitted using the same naming pattern:
   - `medplum.batch.waitingCount`
   - `medplum.batch.delayedCount`
   - `medplum.download.waitingCount`
   - `medplum.download.delayedCount`

2. The new metrics must be sourced from the same underlying queue objects that the respective accessor functions provide (i.e., the same way `getSubscriptionQueue` / `getCronQueue` are used today, but for `getBatchQueue` and `getDownloadQueue`).

3. **Resilience:** If any of the queue accessor functions returns `undefined` or a falsy value (e.g., the queue has not been initialized yet or became unavailable after initialization), the heartbeat listener must not throw, and it must continue to collect and report the metrics for any other available queues that are truthy.

4. **Cleanup:** When `cleanupOtelHeartbeat` is invoked, any cached queue state or references that the listener may hold must be cleared. A subsequent `initOtelHeartbeat` call should re-evaluate queue availability from scratch, never relying on stale cached values.

5. The existing behavior for the `subscription` and `cron` queue metrics must remain unchanged.

feat(otel): `batch` and `download` queue metrics (#7600)


**Title:** `Claim/$submit` does not distinguish prior-authorization claims and cannot accept a `Bundle`

**Description**
When a `Claim` with `use = "preauthorization"` is submitted via `$submit`, it is always dispatched to the custom operation defined by the `CLAIM_SUBMIT_OPERATION` project setting. There is no way to route preauthorization claims to a separate, payer‑specific operation (e.g., a prior‑auth clearinghouse). Additionally, the operation only accepts a bare `Claim` resource; it cannot accept a `Bundle` containing the `Claim` plus supporting resources (such as clinical documents, referrals, or additional patient information), which is a common requirement for prior‑auth submissions.

Finally, the instance‑level endpoint (`Claim/{id}/$submit`) currently only responds to `GET` requests, which is inconsistent with the type‑level `POST` and makes it impossible to pass extra context or a `Bundle` body on that route.

**Expected behavior**
- Introduce a new project setting, `PRIOR_AUTH_SUBMIT_OPERATION`, that holds the code of the custom operation to use when `Claim.use` equals `"preauthorization"`. When that setting is present **and** the claim’s use is `"preauthorization"`, `$submit` should dispatch to that operation instead of `CLAIM_SUBMIT_OPERATION`.
- The `$submit` operation should accept both a raw `Claim` resource **and** a `Bundle` containing a `Claim` (along with any other resources). If a `Bundle` is provided but contains no `Claim`, the server should return a clear error.
- If `PRIOR_AUTH_SUBMIT_OPERATION` is **not** configured and a preauthorization claim is submitted, return a descriptive error indicating that the prior‑auth operation is not set (analogous to the existing error when `CLAIM_SUBMIT_OPERATION` is missing).
- Change the instance‑level route (`Claim/{id}/$submit`) from `GET` to `POST`. The handler should still read the claim from the repository by its ID and forward it to the appropriate custom operation (respecting the same prior‑auth routing logic).

**Non‑obvious constraints**
- The extraction of a `Claim` from a `Bundle` must not fail if the `Bundle` contains additional entries—it should simply locate the first `Claim` resource.
- Existing clients that rely on the instance‑level `GET` will need to switch to `POST`; this is an intentional breaking change for correctness.
- The new setting name must be `PRIOR_AUTH_SUBMIT_OPERATION` (value‑string holding the custom operation code). The existing `CLAIM_SUBMIT_OPERATION` continues to work for non‑preauthorization claims.
- The operation should support both the raw‑resource body style and the FHIR `Parameters` wrapper (as any custom operation does) without requiring callers to wrap `Bundle` or `Claim` in a `Parameters` envelope.

Support prior auth in `Claim/$submit` operation (#9733)


**Implementation Request: `displayLanguage` parameter not honored in `$expand` and `$validate-code`**

The `displayLanguage` parameter for the `ValueSet/$expand` and `CodeSystem/$validate-code` operations is defined but not working correctly:

1. In `ValueSet/$expand`, when `displayLanguage` is provided (e.g., `fr`), the expansion should output the designation text for that language from the ValueSet's inline concept definitions, falling back to the concept's primary `display` only if no matching language designation exists. The text filter should also operate against this chosen display value.

2. The parameter type is currently declared as `boolean`; it should be a `string` (language code).

3. In `CodeSystem/$validate-code`, when `displayLanguage` is supplied, the validation response should return the designation for that language from the stored CodeSystem coding, rather than the default primary display. The underlying coding lookup currently only queries canonical rows (`synonymOf IS NULL`), but when a language is requested it should instead query rows matching that language.

4. The parameter needs to be threaded through the existing validation and expansion helpers so that both operations respect it consistently.

No changes are needed to the operation signatures or the external FHIR API contract; only the internal parameter type definition, query construction, and display-selection logic must be corrected.

Handle display language overrides in ValueSet/$expand and CodeSystem/$validate-code (#8085)


**Ticket: Fix SQL quoting in generated migration files**

**Problem**
The migration generation tool produces broken SQL in some cases:

1. Expression columns in index definitions (for example, `to_tsvector('simple'::regconfig, ...)`) get their single quotes escaped an extra time. This results in syntax errors when the generated SQL is executed.
2. Column names are quoted only when they contain uppercase characters. On PostgreSQL instances where case folding matters, this can cause undefined-column errors for identifiers that are not lowercased implicitly.

**Expected behavior**
- All column names appearing in generated DDL must be properly quoted using PostgreSQL identifier quoting rules (`"columnName"`) so that they work regardless of case.
- String literal expressions inside index definitions should appear exactly as they are specified in the configuration — no additional backslash-escapes should be added to their quotes.

**Additional context**
The generated migration files are written by the server’s schema migration logic. The broken SQL is usually visible inside `CREATE INDEX` statements that contain expression-based columns or `WHERE` clauses, and inside the `idempotentCreateIndex` call arguments written to the migration scripts.

**Acceptance criteria**
- Running the migration generator no longer produces SQL with double-escaped single quotes in `to_tsvector` or similar expression-based index columns.
- All identifiers in the generated SQL are properly wrapped in double quotes.
- Existing functionality that compares or normalizes indexes is not affected beyond the quoting fix.

Fix quote escaping issue in `migrate.ts` (#6632)


**Feature: Scheduler component should support multiple schedules and custom slot search logic**

Currently the Scheduler component only accepts a single `Schedule` (or a reference to one). We need to extend its `schedule` prop so that consumers can:

1. Pass an array of `Schedule` or `Reference<Schedule>` – the component should display available slots aggregated across all schedules.
2. Pass a function `(period: Period) => Promise<Slot[]>` that performs the slot search, allowing completely custom slot‑resolution logic.

**Acceptance criteria**

*Single‑schedule behaviour (unchanged)*
When a single schedule is supplied (by value or by reference), the component works exactly as before, including optionally showing the practitioner avatar and name in the info area.

*Array of schedules*
- The component must search for slots across all provided schedules and display a combined set of time options for the selected date.
- Actor information (practitioner avatar/name) **must not be shown** when multiple schedules are used (there is no single unambiguous actor).
- An empty array must render without errors; the calendar is still usable but no time slots are available on any date.

*Custom slot‑search function*
- When a function is passed to `schedule` the component calls it with the currently visible month’s `Period` and uses the returned slots.
- Actor information **must not be shown** when a custom function is used.
- Returning an empty array must behave naturally: the calendar still allows date selection but no time slots are displayed for that date.

*Duplicate start‑time handling*
If the aggregated slot list contains multiple slots with the exact same start time (e.g. from different schedules or returned by the custom function), only **one** time button should appear for that slot time. No duplicates should be visible in the time‑selection list.

*Workflow integrity*
Selecting a time slot from any of the above scenarios (single schedule, array, custom function) must proceed through the existing questionnaire flow to completion without any change in behaviour after slot selection.

*Calendar day highlighting*
Days that contain at least one available slot from the aggregated result should be highlighted as selectable, matching the current single‑schedule behaviour.

**Non‑goal**
No visual redesign. No changes to the calendar or questionnaire components themselves. Backward compatibility with existing single‑schedule consumers must be preserved.

Update Scheduler Component to handle multiple schedules (#7457)


**Title:** Treat `stream.not.readable` as a warning instead of a 500

**Context**
The server currently logs `stream.not.readable` errors at `error` level and returns HTTP 500. These errors occur when the request body stream becomes unreadable, most often because the client disconnects before the entire body is sent (e.g., AWS ALB returning status 460). This noise masks real server problems and can trigger unnecessary alerts.

**Desired Behavior**
- `stream.not.readable` errors should be logged as a **warning**, not as an error.
- The server should return **HTTP 400** with an appropriate response body (similar to how we already handle `entity.too.large`).
- All other error-handling behavior must remain unchanged.

**Acceptance Criteria**
- When a thrown error has `type === 'stream.not.readable'`, the server no longer falls through to the generic “Unhandled error” path.
- The response status code is 400 (Bad Request).
- The log level is `warn`, containing the error details.
- Existing handling of other known error types (`entity.too.large`, etc.) is unaffected.

Make stream.not.readable a warning (#5321)


**Title:** Consolidate bulk data export job tracking into `AsyncJob` resource

**Background**
We currently use a dedicated `BulkDataExport` resource type to represent bulk export jobs. The platform already has a generic `AsyncJob` resource type designed to track any asynchronous operation; maintaining two overlapping job representations adds complexity and diverges from our goal of a single job lifecycle model.

**Request**
Switch the internal representation of bulk export jobs from `BulkDataExport` to `AsyncJob`. The externally observable behavior of the bulk data export API must not change:

- The status endpoint `GET /fhir/R4/bulkdata/export/:id` must:
  - Serve **legacy `BulkDataExport` records** that already exist in the database. Their existing shape (direct `output`, `error`, `deleted` arrays) must be returned unchanged.
  - Serve **new `AsyncJob` records** created by the updated export flow. For these records the endpoint must rebuild the expected response shape: `output`, `error`, and `deleted` arrays should be extracted from the `AsyncJob`’s `output.parameter` parts (each part has `name` `"output"`/`"error"`/`"deleted"`, and child parts `{name: "type", valueCode}` and `{name: "url", valueUri}`).
- The `BulkExporter` helper (used during export operations) must create and update an `AsyncJob` instead of a `BulkDataExport`, formatting the per-resource-type output entries as `Parameters` parts on the job’s `.output` property.
- The `requiresAccessToken` field on the response should be set to `false` (since rewritten attachments get presigned S3 URLs and don’t need the access token).

**Safety / edge cases**
- **Backward compatibility**: The endpoint must try reading the ID as an `AsyncJob` first, and fall back to `BulkDataExport` if `AsyncJob` is not found. Do not break existing exports.
- **Exclusion list**: Add `'AsyncJob'` to the `unexportedResourceTypes` array that controls which resource types are skipped during export. `BulkDataExport` is already excluded; `AsyncJob` must not appear in a future export’s output.
- The DELETE endpoint (`DELETE /fhir/R4/bulkdata/export/:id`) should continue to return `202` (no change in behavior needed).

**Out of scope**
- Changing the request format or the initial export kickoff endpoint (`POST /fhir/R4/$export`, etc.).
- Modifying the `BulkDataExport` type definition itself—it remains for reading legacy records only.

Use AsyncJob internally for Bulk Data Export API (#5691)


**Ticket: `DiagnosticReport-update` should be rejected when the topic's current context is not a DiagnosticReport**

### Background
FHIRcast `DiagnosticReport-update` events are only meaningful when the active topic context is anchored to a `DiagnosticReport`. The server should validate that condition before processing the context-change request.

### Problem
The FHIRcast endpoint currently accepts a `DiagnosticReport-update` message if *any* current context exists for the topic, even when that context is anchored to a different resource type (e.g., `Patient`). This can lead to unexpected behavior and data inconsistencies because the update handler assumes a `DiagnosticReport` context.

### Expected Behavior
When a `DiagnosticReport-update` context-change request is published:

- If no current context is stored for the topic, or the stored context’s type is **not** `DiagnosticReport`, the server must respond with **HTTP 400** and an `OperationOutcome` whose error details indicate that no `DiagnosticReport` is currently open for the topic.
- The handler must proceed with the update logic **only** when the current context’s type is `DiagnosticReport`.

### Acceptance Criteria
- Submitting a `DiagnosticReport-update` event on a topic whose current context is not `DiagnosticReport` (including missing context) returns 400.
- Submitting the event on a topic with a valid `DiagnosticReport`-typed context continues to work unchanged.
- The error response matches the existing pattern used for this guard (the same message already used when a context is absent).

### Notes
- The fix should be minimal and localized to the `DiagnosticReport-update` handler’s context validation. No changes to storage, serialization, or other event types are needed.
- Consider whether the current context retrieval function enforces a type at runtime; TypeScript generics alone do not provide a runtime guarantee, so an explicit check is necessary.

fix(fhircast): prevent `DiagnosticReport-update` in other cxts (#8789)


```markdown
## Feature Request

### Summary
Automatically create and assign a default patient access policy when a new project is provisioned.

### Background / Motivation
Currently, new projects are created without a `defaultPatientAccessPolicy`. This means the `/auth/newpatient` endpoint (patient self-registration) fails for a new project until an administrator manually:
1. Creates an `AccessPolicy` resource with appropriate patient-compartment restrictions.
2. Patches the project to set `defaultPatientAccessPolicy` to that policy.

This is not obvious and creates friction. We should ship new projects with a sensible default policy so that patient registration works out of the box. If an admin later removes or replaces the policy, existing behavior should still apply (patient registration fails without a policy).

### Desired Behavior
Whenever a project is created (whether via the FHIR `$init` operation, the new-project signup flow, or any other programmatic path that creates a project), the system should:

1. **Create a default patient access policy** as a system resource owned by the project. The policy must:
   - Use a **patient compartment** (`compartment: { reference: '%patient' }`).
   - Grant access to common clinical resource types that a patient should be able to see/read about themselves. At minimum include:
     - `Patient`
     - `Observation`
     - `Condition`
     - `MedicationRequest`
     - `DiagnosticReport`
     - `CarePlan`
     - `Immunization`
     - `Encounter`
     - `Procedure`
     - `AllergyIntolerance`
     - `DocumentReference`
     - And other relevant FHIR resources that are commonly patient-facing.
   - Use appropriate search criteria, either `?_id=%patient.id` for `Patient` or `?_compartment=%patient` for other compartment-eligible types.
2. **Set the project's `defaultPatientAccessPolicy`** to a reference to the newly created policy.

### Constraints & Edge Cases
- The policy should be created via the **system repository** so it is not subject to the creating user's own access restrictions, and it must be correctly scoped to the project (i.e., `meta.project` set).
- The policy should **not** be removable or editable by patients; it is an administrative artifact.
- If an admin later **removes** the `defaultPatientAccessPolicy` from the project (or sets it to `null`/`undefined`), patient registration must fail as it did before — no regression.
- Existing projects are **not** retroactively affected; only newly created projects get the default.
- The policy creation must be part of the same transaction/flow as project creation to avoid a window where no policy is set.

### Acceptance Criteria (observable)
- After a new project is created (e.g., via `$init` on Project), reading the project shows a valid `defaultPatientAccessPolicy` reference pointing to an `AccessPolicy` resource that contains the expected compartment and resource rules.
- Patient registration (`POST /auth/newpatient`) succeeds immediately after project creation without any additional manual steps.
- When the default policy is later removed from the project, patient registration again returns an error (preserving existing contract).
- Existing tests that previously had to manually set up a policy for patient registration are updated to reflect that the policy is now automatic (or that removal of the policy causes the expected failure).
```

Automatically provision default patient access policy in new projects (#9263)


As a user, I often need to display all applicable reference ranges for a patient, not just the first one. The current `findObservationReferenceRange` helper returns only the first matching interval. We should add a corresponding `findObservationReferenceRanges` that returns all matching reference ranges.

Requirements:

- `findObservationReferenceRanges(definition, patient, names?)` should return an array of all qualified intervals for the patient.
- The `names` parameter should be optional. When provided, filter intervals to those whose condition is in `names`. When omitted, return all intervals that match the patient regardless of condition.
- Match the same patient matching semantics as `findObservationReferenceRange` already uses — only intervals whose criteria apply to the patient should be returned.
- If no intervals match (or if `definition.qualifiedInterval` is undefined/empty), return an empty array.
- Update `findObservationReferenceRange` to delegate to the new function (taking its first element) and make its `names` parameter optional for consistency.

Fixes #5733 - added new findObservationReferenceRanges (#6046)


In `packages/core/src/websockets/reconnecting-websocket.ts`, the `ReconnectingWebSocket` wrapper and its `IWebSocket` interface have a few problems that break interop with real-world `WebSocket` implementations and cause inconsistent error delivery:

**1. `IWebSocket` interface is too brittle for alternative implementations**
The interface currently declares `onopen`/`onclose`/`onmessage`/`onerror` handler properties using `any[]` args. Those properties aren’t actually needed (the wrapper always uses `addEventListener`) and the loose signatures cause contravariance failures with libraries like `ws`, whose events use `target: WebSocket` rather than `target: EventTarget`. The `addEventListener`/`removeEventListener` overloads are also tied to the local `WebSocketEventMap`, which doesn’t match the shapes emitted by some conformant implementations.
→ **Required**: Remove the `onX` handler properties from `IWebSocket`, and wire the typed `addEventListener`/`removeEventListener` overloads to `IWebSocketEventMap` (global event interfaces) while keeping the permissive `string`-typed overload for compatibility.

**2. `error` event shape varies across environments and isn’t handled safely**
- Browsers dispatch a plain `Event` with no `message` or `error` property.
- Node's `ws` and built-in `WebSocket` (undici) dispatch an `ErrorEvent` with both `message` and `error`.
- Bun dispatches a plain `Event` with a `message` string but no `error` object.

The internal `_handleError` blindly uses `event.message`, which fails for browser-style events, and passes the raw event to `onerror` and `dispatchEvent`, so downstream code sees different shapes depending on the runtime.
→ **Required**: Normalize the raw error event into a proper `ErrorEvent` (carrying a real `Error`) before `_handleError` uses it, invokes `this.onerror`, and dispatches it. The normalized event must always have both `error` and `message` populated, preserving the original `Error` instance when available and falling back to a generic message.

**3. Event cloning corrupts polyfilled `CloseEvent` and `ErrorEvent`**
The `cloneEvent` helper reconstructs events using `new (e as any).constructor(e.type, e)`, following the DOM `(type, eventInitDict)` convention. But the polyfill `Events.CloseEvent` constructor has a `(code, reason, target)` signature and `Events.ErrorEvent` has `(error, target)`. Because of this mismatch, a cloned close event ends up with `code: 'close'` and its `reason` set to the entire event object, breaking close-event handling. Similarly, cloned error events lose their error details.
→ **Required**: `cloneEvent` must handle `CloseEvent` and `ErrorEvent` polyfills specially so that cloned close events preserve `code` and `reason`, and cloned error events preserve `error` and `message`.

**Constraint**: The fix must not change the public interface of `ReconnectingWebSocket` (users can keep using `onopen`, `onclose`, etc. as before), and existing tests should continue to pass after the changes. The normalized error behavior should be testable with the `vitest-websocket-mock` `WS` server, which produces a browser-style plain `Event` for errors.

---

Implement the fixes in `packages/core/src/websockets/reconnecting-websocket.ts` and add corresponding test cases in `packages/core/src/websockets/reconnecting-websocket.test.ts` to verify:
- The `IWebSocket` type changes remain compatible with `ws`, browser `WebSocket`, and Node built-in `WebSocket`.
- Error events from the mock are normalized to an `ErrorEvent` with `error` and `message`.
- Close events from `reconnect()`/`close()` preserve their `code` and `reason`.
- The `onX` handler and `addEventListener` listener receive distinct event objects (original vs. clone) and both carry the correct data.

fix(core/ws): fix `ReconnectingWebSocket` types, fix error handler shape (#9461)


**Title:** Allow clients to opt-in to `WWW-Authenticate` on 401 responses

**Context**
The server currently never sends a `WWW-Authenticate` header when returning a 401 for an unauthenticated request. This was deliberately done to avoid the browser’s native basic-auth dialog, which is triggered automatically when that header is present. The dialog is a poor experience for users of the Medplum web app and other browser-based clients.

However, there are programmatic and non-browser clients (e.g., testing tools, headless API consumers) that *do* want a `WWW-Authenticate` challenge so they can initiate a Basic authentication flow. We need a way to serve both use-cases.

**Request**
Add support for a special query parameter that, when present, causes the server to include the `WWW-Authenticate` header on 401 responses to unauthenticated requests.

- The parameter should be named `_medplum-prompt-basic-auth`. Any truthy value (e.g., `=1`) must trigger the behavior; the value itself need not be inspected beyond existence.
- When the parameter is present and the request fails authentication, the response must include:
  `WWW-Authenticate: Basic realm="<base-url>"`
  where `<base-url>` is the server’s configured base URL (available from the app configuration).
- When the parameter is **absent**, the 401 response must **not** include a `WWW-Authenticate` header. This maintains the status quo and keeps browsers from showing the unwanted basic-auth dialog.
- The change should only affect the 401 path for unauthenticated requests; authenticated requests should be unaffected regardless of the parameter.

**Non-obvious edge cases / constraints**
- The parameter may appear in the query string of any route that goes through the authentication middleware; make sure the check works consistently.
- Do **not** change the behavior when the parameter is missing — the header must stay absent exactly as it is today.
- The realm must use the same base URL that the server is actually running on (derived from config), not a hardcoded string, so deployments with different base URLs still get the correct realm.

Conditionally include WWW-Authenticate header if magic query param included in request (#5863)


**Title:** Add `Prefer: respond-async` support to `Bot/$execute`

The `Bot/$execute` operation currently always runs synchronously and returns the bot output directly in the HTTP response body. We want to extend it to respect the `Prefer: respond-async` header, enabling long-running bots to be executed without holding the client connection.

### Expected behavior

When a client sends `Prefer: respond-async`:

1. The server **immediately returns** `202 Accepted` with a `Content-Location` header pointing to an `AsyncJob` resource.
2. The bot execution proceeds in the background, and the `AsyncJob` is updated with the final result.
3. The `AsyncJob.output` must be a `Parameters` resource that captures the bot’s return value or an `OperationOutcome` if the execution fails.

### Output shape (`AsyncJob.output`)

The output `Parameters` must mirror the bot’s actual return value in a structured way:

- **String or number return**: a parameter with `name: "responseBody"` and the value as a FHIR string (`valueString`). Numbers should be converted to string (matching the existing sync behaviour).
- **Boolean return**: a parameter with `name: "responseBody"` and `valueBoolean`.
- **Object return**: a parameter with `name: "responseBody"` and the value serialised as a JSON string (`valueString`).
- **Error / OperationOutcome**: when the bot returns an `OperationOutcome` or throws, the async job should transition to `status: "error"` and include a parameter with `name: "outcome"` containing the `OperationOutcome` resource.
  - This covers validation failures (e.g. missing bot ID) as well as bot runtime errors.
- **Undefined/void return**: currently the sync path interprets `undefined` as success and returns an appropriate `OperationOutcome` (`allOk` or an error depending on `result.success`). The async path should do the same: put the effective `OperationOutcome` into the output as an `outcome` parameter.

### Backwards compatibility

Requests **without** the `Prefer: respond-async` header must continue to work exactly as they do today – the existing sync response flow must not change.

### Implementation notes

- The endpoint should reuse the existing `AsyncJobExecutor` infrastructure already used by operations like `Group/$export`.
- The `AsyncJob.request` field should contain the original `$execute` request URL.
- The implementation must correctly propagate errors that occur before the bot runs (e.g. invalid bot ID, unsupported runtime version) into the async job error output.
- Existing sync tests must keep passing without modification – this is strictly additive.

feat(bot): add support for `Prefer: respond-async` on `Bot/$execute` (#4461)


**Title:** Healthcheck does not report metrics for the read‑replica database instance

**Problem**
The `GET /healthcheck` endpoint emits gauges for database idle connections and queries awaiting a client, but these metrics currently only cover the primary (writer) database pool. In deployments that use a read‑replica (the `readonlyDatabase` configuration), there is no visibility into the connection pool behaviour of the reader instance.

**Expected behavior**
- The healthcheck should expose the same pool metrics (`idleConnections`, `queriesAwaitingClient`) for the reader instance, tagged with an attribute (e.g. `dbInstanceType`) to distinguish `writer` and `reader`.
- When no separate read‑replica is configured (the writer and reader pools are the same), duplicate metrics must **not** be emitted — only a single set with no reader‑specific label, or both labels pointing to the same value, but without double‑counting.
- The `readonlyDatabase` config field should be optional. A deployment that only specifies `database` should still work and not require a separate reader database definition.

**Impact**
Monitoring dashboards and alerts that rely on these pool metrics currently have a blind spot for the read‑replica, making it harder to detect connection saturation or under‑provisioning on the reader side. Making `readonlyDatabase` optional also simplifies configuration for smaller or single‑instance setups.

metrics(db): track reader instance stats if reader configured (#4845)


As a developer using the Medplum FHIR API, I need the PlanDefinition `$apply` operation to support an optional Encounter parameter, so that when a PlanDefinition is applied in the context of a specific patient visit, the created resources are linked to that encounter.

Acceptance criteria:

1. The `POST /fhir/R4/PlanDefinition/{id}/$apply` endpoint currently accepts a `subject` parameter (a Patient reference). It should also accept an optional `encounter` parameter, a FHIR reference to an Encounter resource.
2. When the `encounter` parameter is provided:
   - The resulting `RequestGroup` resource must include an `encounter` field referencing the supplied Encounter.
   - Every `Task` resource created as part of executing the plan's actions must have its `encounter` field populated with the same Encounter reference.
3. If the `encounter` parameter is omitted, the operation must continue to work as before (no encounter reference on the RequestGroup or Tasks).
4. All other existing behavior of the `$apply` operation must be preserved, including creation of Tasks for Questionnaire actions (with appropriate `input` references) and non-Questionnaire actions.
5. The encounter reference provided must be resolved to a valid Encounter resource; if it is invalid or cannot be found, the operation should return an appropriate error (standard FHIR behavior for invalid references).

This feature is needed to support encounter-specific plan workflows, allowing generated tasks to be directly associated with a patient’s current visit.

Include encounter to plan definition API (#5800)


**Title:** `nonBlockingAddConstraint` (used in post-deploy migrations) is not idempotent, causing duplicate constraint errors on re-run

**Observed behavior**
Post-deploy migration v25 calls `nonBlockingAddConstraint` to add a `CHECK` constraint (`reserved_project_id_check`) to the `Project` table. When the migration is executed a second time (for example, when the service is re-deployed or the migration is re-triggered), the operation fails with a database error indicating that the constraint already exists.

This means any migration that uses `nonBlockingAddConstraint` cannot be safely re-run, which breaks the expected migration contract that migrations should be idempotent.

**Expected behavior**
The non‑blocking constraint helper should be idempotent. Specifically:

- If the named constraint **does not exist** on the target table, it should be created with `NOT VALID` and then validated, just as it is today.
- If the constraint **already exists and is valid**, the function should do nothing and return successfully.
- If the constraint **already exists but is marked `NOT VALID`** (i.e., it was added in a previous run but not yet validated), the function should skip the `ADD CONSTRAINT` step and proceed directly to `VALIDATE CONSTRAINT`.

In all cases, the operation should succeed without error when the desired constraint is already present, regardless of whether it is currently valid or invalid.

**Impact**
This effectively prevents a migration from being safely re‑executed in any environment where the constraint may have been partially applied. It also means any future `CHECK` constraint migration must be written with extra boilerplate to check for existence, defeating the purpose of a shared helper.

**Additional note**
The current name `nonBlockingAddConstraint` is too broad (the function only adds `CHECK` constraints, not other types). A rename to `nonBlockingAddCheckConstraint` would better communicate its scope and help avoid misuse. This can be done as part of the idempotency fix.

nonBlockingAddCheckConstraint is idempotent (#7353)


**Title:** Batch FHIR processing should stop on first rate‑limit error instead of continuing to hit the limit for each remaining entry

**Description:**

When a client submits a FHIR `batch` bundle and the server’s rate limit (FHIR quota) is exceeded part‑way through processing the entries, the current behaviour is to execute every remaining entry individually. Each of those subsequent entries immediately fails with a `429 Too Many Requests` error because the quota has already been exhausted.

This is wasteful and creates unnecessary load on the server. From the client’s perspective it also makes the response harder to act on, because the batch still runs to completion but produces a string of identical 429 outcomes for every future entry.

**Expected behaviour:**

Once any entry in a `batch` operation results in a rate‑limit error (HTTP `429`), the batch processor should:

- stop executing further entries,
- fill the response for every remaining unprocessed entry with the same `429` OperationOutcome,
- and return the batch‑response `Bundle` as usual (HTTP `200` wrapping the `Bundle`).

All successfully‑executed entries should still appear in the response with their normal status (e.g. `201`). The batch response must contain exactly one entry per original bundle entry, preserving entry order and structure, so that clients can reliably map results back to requests.

**Non‑goal / existing behaviour to preserve:**

- Non‑rate‑limit errors (e.g. validation failures, `400`/`409`, etc.) should continue to be handled per‑entry and should **not** stop the rest of the batch.
- Transaction bundles (`type: "transaction"`) are outside the scope of this request; only FHIR `batch` processing needs this rate‑limit fast‑fail behaviour.

**Reproduction sketch (what a client might do):**

Send a `POST /fhir/R4/` with a `Bundle` containing several `POST Patient` entries. If the project’s FHIR quota is configured so that the limit is hit after the first entry, the response currently contains `201` for the first entry and `429` for every entry that follows, all of which were still executed by the server. The desired response would be identical in structure but the server would have skipped execution of the entries after the first `429`.

**Why this matters:**

- Reduces unnecessary server work under load.
- Gives client developers a clearer signal: the first `429` can be seen as a “stop‑point” for retries rather than having to parse a whole stream of repeated errors.
- Better aligns with common “fail‑fast” patterns for rate‑limited API calls.

Fail batch processing on first rate limit error (#6657)


We need to gate the `$get-ws-binding-token` FHIR operation so it only succeeds when the requesting project has the `websocket-subscriptions` feature flag enabled.

Currently, any authenticated project can obtain a WebSocket binding token via `POST/GET [base]/Subscription/:id/$get-ws-binding-token`, regardless of whether websocket subscriptions are enabled for that project. That should be tightened: if the project’s feature list does not include `'websocket-subscriptions'`, the endpoint must reject the request with a `400 Bad Request` `OperationOutcome` error (severity `error`, code `invalid`). The exact error message can follow existing patterns used for other feature gates in the server.

For projects that do have the feature, the endpoint behaviour must remain unchanged—a valid binding token is returned as before.

No other endpoints or flows should be affected; the websocket connection itself can continue to rely on whatever checks it already has. The gate belongs on the token-issuing operation only.

feat(getwsbindingtoken): add feature gate on `websocket-subscriptions` (#4193)


**Title:** Bulk export does not check user read permissions on resource types

**Description:**
The system-level and patient-level bulk export handlers currently iterate over the resolved set of resource types and only filter out types that are not exportable (`canBeExported`) or not explicitly requested via the `_type` parameter. They do not verify whether the authenticated user actually has read access to each resource type. This can lead to exporting data for resource types that the user is not authorised to view.

We need to add a permission guard in the resource type selection loop so that types the user cannot read are silently skipped, just as non-exportable types are skipped today.

**Where & how:**
The logic lives in `packages/server/src/fhir/operations/export.ts`, in the `exportResources` function that iterates over the candidate resource types and calls `exportResourceType`. The `BulkExporter` instance already carries a `repo` object that exposes a `canReadResourceType(resourceType)` method for exactly this kind of authorisation check.

Add a condition in the existing filter `if` block so that a resource type is also skipped when `exporter.repo.canReadResourceType(resourceType)` returns `false`. The existing checks (`canBeExported` and the `_type` filter) should remain unchanged.

No other export functionality (job creation, output formatting, etc.) needs to change; only the resource type eligibility check should be tightened.

Check user read permissions in export operation (#4076)


When creating or updating a resource that contains a reference starting with `#` (a local contained reference per FHIR), the reference validation step rejects it with an error. For example, a Questionnaire whose `valueReference` is `{ "reference": "#template" }` fails to save.

We already skip reference validation for references starting with `%`. Extend that behavior to also skip references that start with `#`. References that begin with `#` should be treated as contained, local references and never validated as external resource references.

Exclude `#` symbol on check references  (#7582)


Refactor `searchByReferenceImpl` in the FHIR search layer to generate separate queries per reference value and combine them via `UNION ALL`, instead of using a `VALUES` table with a `LATERAL` join. The new approach must:

- For each reference value, produce a subquery that has the reference value substituted into the search condition and that includes a literal column for the reference value.
- Return rows with the same three columns as today: `id`, `content`, and `ref`.
- Preserve grouping of results by reference value (the caller already handles this; just ensure the rows contain the necessary data).
- Handle reference fields backed by array columns (the `ARRAY_OVERLAPS_AND_IS_NOT_NULL` operator) identically to scalar columns.
- Reuse the existing `getSelectQueryForSearch` / `builder` logic; do not re-implement filtering.

In the SQL builder layer, add appropriate support for constructing `UNION ALL` queries. In the multi-resource-type `getBaseSelectQuery` path (where a `Union` is used), change the select list to use `*` so that all columns—including any extra literal columns added by the caller—are carried through.

Use `UNION ALL` instead of lateral joins (#6923)


**Title:** Bot `$execute` returns Binary as FHIR JSON instead of raw binary data

**Problem**
When a Medplum Bot returns a `Binary` resource (e.g., a generated PDF, an image, or any file) the `$execute` endpoint always sends back the FHIR JSON representation of the `Binary` resource. There is no way to receive the actual binary content directly from a bot invocation.

**Current behavior**
- After executing a bot whose result is `{ resourceType: 'Binary', contentType: 'application/pdf', data: '...' }`, the client receives `application/fhir+json` with the `Binary` structure as the body.
- The `Accept` header is effectively ignored: even when the client does not pass `Accept: application/fhir+json`, it still gets FHIR JSON.

**Expected behavior**
The bot `$execute` endpoint should handle `Binary` return values the same way the existing `GET /fhir/R4/Binary/:id` endpoint handles Binary reads:
- If the request’s `Accept` header starts with `application/fhir+json`, respond with the `Binary` resource in FHIR JSON format.
- Otherwise, respond with the raw binary data, using the `Binary.contentType` as the response `Content-Type`. The response body should be:
  - The decoded `data` field (base64) when the `Binary` has inline data, or
  - The stored binary stream (piped) when the data is held in external storage.
- The HTTP status code should still reflect the bot execution outcome (200 for success, 400 for a bot error), analogous to how other bot results are already handled.

**Why this matters**
Bots that produce files are currently forced to save the `Binary` resource separately and then have the client issue a second request to the read endpoint. Returning the binary payload directly from execution simplifies workflows, reduces latency, and aligns with developer expectations that a bot can behave like an on-the-fly content generator.

**Additional note**
The existing `GET /fhir/R4/Binary/:id` implementation already contains the correct content-type and Accept-header logic. Reusing that behavior for bot results would be the most consistent and maintainable approach.


**Title:** OAuth2 token endpoint incorrectly requires PKCE verification when a valid client secret is provided

**Behavior:**

After a recent change, confidential clients that present a valid `client_secret` during an authorization-code token exchange (`POST /oauth2/token`, `grant_type=authorization_code`) may still be rejected with a PKCE-related error if the original authorization request happened to include a `code_challenge` and `code_challenge_method`. The token endpoint should treat a successfully validated client secret as sufficient authentication and **not** require a `code_verifier` when the secret check passes. Public clients (no secret) that are not marked as PKCE-optional must continue to enforce PKCE verification as before.

Separately, the authorization server’s request validation for PKCE parameters has regressed; it lets through some invalid combinations that should be rejected.

**Acceptance criteria:**

1. **Token exchange – client secret takes priority:**
   - When a token request includes a `client_secret`, the endpoint must first validate the secret against the client application.
   - **If the secret is valid**, the request must succeed (subject to other non-PKCE checks) **regardless of whether the original login used PKCE**. No `code_verifier` is required in this case.
   - If the secret is invalid or missing, and the client is **not** PKCE-optional, then PKCE verification proceeds as normal (a `code_verifier` must be provided and match the stored `code_challenge`).

2. **Authorization request validation (login):**
   - Reject requests where `code_challenge` is present but `code_challenge_method` is absent (or blank).
   - Reject requests where `code_challenge_method` is present but `code_challenge` is absent.
   - Only allow `"plain"` and `"S256"` as valid values for `code_challenge_method`. Any other value must be rejected with an appropriate error.

3. **Non-regression:**
   - Existing tests covering successful exchanges without PKCE (using client secret) and successful exchanges with PKCE (public clients) must continue to pass after the fix.

Revert "Always verify PKCE on token grant if present (#5795)" (#5845)