ihe-iti 0.0.44

The IHE IT Infrastructure (ITI) identity profiles: PIXm (ITI-83), PDQm (ITI-78, ITI-119), mCSD (ITI-90, ITI-91), PMIR (ITI-93, ITI-94) and XCPD (ITI-55), one feature per profile, with their audit records behind `balp` and `atna`
Documentation

ihe-iti

The IHE IT Infrastructure (ITI) identity profiles in Rust: one crate for the technical framework, with a feature per profile. It is published from FerroPIX, the FerroHEALTH Master Patient Index, which adds the server sides of the profiles as features.

Feature Profile Transactions
pixm Patient Identifier Cross-reference for Mobile ITI-83
pdqm Patient Demographics Query for Mobile ITI-78, ITI-119
mcsd Mobile Care Services Discovery ITI-90, ITI-91
pmir Patient Master Identity Registry ITI-93, ITI-94
xcpd Cross-Community Patient Discovery ITI-55
balp The BALP AuditEvent each FHIR profile above defines for its transactions, recorded on the audit trail of ihe-atna ITI-83, ITI-78, ITI-119, ITI-90, ITI-91, ITI-93, ITI-94
atna The ITI-55 DICOM audit message of xcpd, in ihe-atna's ATNA model, with no FHIR model ITI-55

The crate depends on no application, so a federation gateway, a master patient index or any other caller can use it as it is. Only the xcpd feature may carry the SOAP 1.2, HL7 v3 and SAML XUA stack; a build without it compiles none of it. The profiles are published at https://profiles.ihe.net/ITI/.

Access tokens (IUA ITI-72)

The PIXm, PDQm, mCSD and PMIR clients send the credential their reqwest::Client carries in its default headers, or, built .with_authorizer(…), ask an ihe_iti::authorizer::Authorizer for the headers of every request: an OAuth 2.0 access token incorporated as IUA ITI-72 §3.72.4.2 asks, which the authorizer refreshes before it expires. The authorizer reads every answer, and a request it asks for again after a 401 is sent once more with new headers (§3.72.4.3), never more than twice in all. The authorizer is handed the request URL without its query, so no patient identifier or search criterion reaches it, and a request it cannot authenticate is not sent.

PIXm (pixm)

ihe_iti::pixm::PixmClient is the Patient Identifier Cross-reference Consumer of ITI-83 (PIXm 3.1.0): GET [base]/Patient/$ihe-pix with one sourceIdentifier and any number of targetSystem domains, read into a cross-reference, the profile's not-found answer, or a typed error. A client built with .invoked_by(Invocation::Post) posts the same parameters in a Parameters body to [base]/Patient/$ihe-pix instead (FHIR R4 Operations §3.2.0.1), so the source identifier is in no request URL; ITI-83 prescribes the GET, the default, so post only to a Manager that accepts it. A 404 is "patient unknown" only when its OperationOutcome carries a not-found issue, so an outage or a misrouted request is never mistaken for a patient with no identifiers. Every answer is held to the $ihe-pix OperationDefinition: the two out parameters, an assigning authority on each identifier, identifiers only from the domains asked about, and never the source identifier itself.

Identifier values travel in secrecy::SecretString, with redacted Debug and no Display, and no error carries a value, the request URL or body, or the Manager's free text. Build the reqwest::Client you pass in with redirect::Policy::none(): the request holds the source identifier. The client's Debug shows its URL with the userinfo replaced by *** and leaves out the reqwest::Client, whose default headers may hold a credential.

PDQm (pdqm)

ihe_iti::pdqm::PdqmClient is the Patient Demographics Consumer of ITI-78 (PDQm 3.2.0): a POST [base]/Patient/_search with the criteria of a PatientQuery (every Patient search parameter ITI-78 names, with :exact on the string ones), read into a page of matching Patients with the total, each match's fullUrl, search.score and match-grade, the warnings of any OperationOutcome entry, and the next link that next_page follows on the Supplier's own origin. No match is a total of 0; a 404 is "identifier domain not recognised" only when the query names a domain and the OperationOutcome carries a not-found issue. Every answer is held to the Query Patient Resource Response Message profile: a searchset with a total, and a fullUrl on every entry. The Patients are decoded as FHIR R4, not held to the PDQm Patient profile, as the profile asks of a Consumer.

The same client is the Consumer of ITI-119, Patient Demographics Match: match_patient posts a MatchInput (the input Patient's identifiers, with onlyCertainMatches and count when set) in a Parameters resource to [base]/Patient/$match, and reads the Match Output Bundle into the matched Patients, most likely first, each with its fullUrl, its score between 0 and 1 and its match-grade. No match is an answer with no Patient. An answer is refused when a Patient entry lacks its search mode match, its score or its grade, or when a 200 carries an OperationOutcome of error or fatal severity.

The criteria travel in the request body, so no URL carries them. They, every matched Patient and every page link redact their content in Debug, with no Display, and no error carries a value, a URL or the Supplier's free text. Build the reqwest::Client you pass in with redirect::Policy::none(). The client's Debug shows its URLs as the PIXm client's does.

mCSD (mcsd)

ihe_iti::mcsd::directory::Directory reads the Organization and Endpoint resources a care services directory publishes (mCSD 4.0.0) from one FHIR R4 JSON Bundle of type collection or searchset. It resolves the references between them inside the Bundle as FHIR R4 §2.36.4.1 does: a relative [type]/[id] against the root of the referring entry's REST fullUrl, an absolute reference against an entry's fullUrl. A reference it cannot resolve is reported as outside the Bundle, never guessed. It refuses an entry of another resource type, a resource with a modifierExtension, and a repeated fullUrl or logical id. Each resource stays as fhir-types decodes it, with accessors for what addressing reads; what a caller accepts as a connection type, a status or an identifier is the caller's policy. The Debug of an entry shows its fullUrl, and an endpoint's address, with the userinfo and the query replaced by ***, and leaves out an endpoint's header list.

ihe_iti::mcsd::client::McsdClient asks a directory over HTTP. find is ITI-90, Find Matching Care Services: GET [base]/Organization or GET [base]/Endpoint with the search parameters you give, answered with a searchset Bundle. updates is ITI-91, Request Care Services Updates: GET [base]/[type]/_history?_since=[instant], answered with a history Bundle, newest version first, a deletion as a DELETE entry. Both follow every next link on the directory's own origin and return the Date the directory stamped its first page with, as written. Every walk draws on a budget::Budget: one deadline for the whole walk, and caps on the pages, the answer bytes and the entries it reads (by default 200 pages, 64 MiB and 50,000 entries). Running out of any of them is an error, never a shorter answer. A status other than 200, an answer that is not the interaction's Bundle, a match of another type and a page link to another origin are errors that carry no URL and no text of the directory's. Build the reqwest::Client you pass in with redirect::Policy::none().

ihe_iti::mcsd::replica::Replica holds a directory's Organizations and Endpoints in a Scope (every resource, or those carrying an identifier in a system you name), read with ITI-90 and kept in step with ITI-91 from an instant you choose, usually from the Date it keeps of its last read. The newest version of each resource wins, a deletion or a version that leaves the scope removes it, and a refresh with no instant reads everything again. A refresh returns a new replica and leaves the old one as it was, and directory answers the content as a Directory.

XCPD (xcpd)

ihe_iti::xcpd::XcpdClient is the Initiating Gateway of ITI-55, Cross Gateway Patient Discovery (ITI TF-2 §3.55, Revision 20.1): a PRPA_IN201305UV02 in a SOAP 1.2 envelope with the WS-Addressing headers of Appendix V, posted to one Responding Gateway, which asks by the shared patient identifier alone (the identifier mode of §3.55.1, no demographics). The answer reads into a Discovery for Cases 1 to 4 of §3.55.4.2.3, each match with the homeCommunityId that holds it, or a typed error for Case 5, a SOAP fault, a status with no answer, a timeout, and any answer that does not hold to the transaction: a RelatesTo naming another request, a match with no community, a NF with matches, a document type declaration. Only the synchronous exchange with an immediate response is offered.

A RespondingGateway is https only, since the request carries the patient identifier and any XUA assertion; unencrypted_for_development admits http for development and tests. The reqwest::Client you pass in carries the mutual TLS of the ATNA Secure Node the actor is grouped with; build it with redirect::Policy::none().

The crate signs nothing. XuaAssertion holds a SAML 2.0 assertion your identity provider signed, checked to be exactly one saml2:Assertion element that declares every prefix it uses, and the client sends its bytes unchanged in a WS-Security header. The patient identifier, every identifier an answer returns, and the assertion redact their content in Debug, and no error carries a value or the gateway's free text.

XcpdClient::audited hands the ITI-55 Initiating Gateway audit message of every exchange (§3.55.5.1.1) to an AuditRecorder you route to your ATNA audit repository; the client awaits record before it returns the answer. The message carries the query parameters, which name the patient, as a SecretString. A message the recorder refuses fails the discovery with XcpdError::Audit, so no answer is used without its audit. With atna, AuditEvent::message writes it as the DICOM PS3.15 audit message the Initiating Gateway's table fills, in ihe-atna's ATNA model.

The XML is read and written with quick-xml, which the FHIR features compile already through fhir-types; xcpd adds only uuid for the message ids and jiff for the creation time, and no other feature but balp and atna, which write timestamps too, compiles either.

Whom an exchange is made for

PixmClient::cross_reference, the PdqmClient searches, pages and matches, and XcpdClient::discover each take an ihe_atna::user::OnBehalfOf, the user model the audit trail shares: the User an OAuth 2.0 access token names (its iss, sub, client_id, aud and purposes of use), or System for an exchange the client's own system makes. An audited exchange's record names the user: the AuditEvent of the balp feature carries the BALP agent:user and an Application agent with the client_id (PIXm §2:3.83.5.2.1, BALP 1.1.4 §3:5.7.5.4), and the ITI-55 message carries the Human Requestor, with UserName written aud<sub@iss> (ITI TF-2 §3.55.5.1.1, IUA ITI TF-2 §3.72.5.1). A System exchange names no user. User's Debug shows none of its values. The mCSD and PMIR exchanges are always the system's own.

BALP (balp)

With balp, every PIXm, PDQm, mCSD and PMIR client takes an ihe_atna::balp::AuditRecorder through .audited(…) and records the BALP AuditEvent its profile defines for each transaction: the PIXm Query Consumer of ITI-83, the PDQm Query and Match Consumers of ITI-78 and ITI-119, the mCSD Query and Updates Clients of ITI-90 and ITI-91, and the PMIR Feed Consumer and Subscriber of ITI-93 and ITI-94. The client awaits the recorder before it returns, its timeout bounds the record too (ihe_atna::balp::recording::Late), and a record the recorder refuses fails the exchange as an audit failure, so no answer is used without its audit. The audit trail itself, ATNA ITI-20 with its syslog sender, spool and forwarder and the ATX: FHIR Feed, is the ihe-atna crate of FerroAUDIT.

Licence

Business Source License 1.1 (LICENSE): free for every non-production use and for non-commercial production use; a commercial licence for other production use; Apache License 2.0 four years after each version.