Skip to content

0.0.3

Choose a tag to compare

@github-actions github-actions released this 02 Oct 05:20
· 626 commits to main since this release
Immutable release. Only release title and notes can be modified.
v0.0.3
e9c7e5b

The first two federated milestones in one release (v0.0.2 and v0.0.3; no
v0.0.2 tag was cut). POST {base}/v1/query/aql answers one ITS-REST
RESULT_SET over two openEHR CDRs, with the patient resolved outside AQL
through a PIXm PIX Manager (or the development cross-reference) on either
patient carrier, and no directly identifying identifier sent to a node: the
rewrite refuses it, and an outbound gate re-checks every request before it
leaves. Also the Cargo workspace and its spec-named crates, the PIXm and PDQm
clients, the container and compose quickstart, the end-to-end harness with a
PIX Manager, the fuzz lane, and the release lane at SLSA Build Level 3,
rehearsed as v0.0.2-rc.1.

Added

  • The PDQm ITI-78 patient demographics query client in ihe-iti, feature
    pdqm (#119; PDQm 3.2.0, ITI TF-2 §2:3.78). pdqm::PdqmClient posts a
    PatientQuery to [base]/Patient/_search as a form body, so no URL carries
    a demographic value, and reads the searchset into the matching Patients
    (FHIR R4 Patient from fhir-types resources), the total, each match's
    score and match-grade, the OperationOutcome warnings and the next page
    link, which it follows only on the Supplier's origin. A 404 with a
    not-found issue for a query that names an identifier domain is the
    profile's unrecognised-domain answer; every other failure is a typed error
    that carries no value, URL or Supplier text. The ITI-78 artefacts of the PDQm
    3.2.0 package are vendored under docs/specs/ihe-pdqm/ by
    scripts/vendor/ihe-pdqm.sh. The OperationOutcome issue type moves to
    ihe_iti::outcome::IssueType, shared by PIXm and PDQm; ihe-iti is 0.0.5.
  • The identity-lifecycle hook for track 8 (#48): ResolutionBindings::identity_changed
    drops every resolution binding a merge or split at the identity source could
    have made stale, by ehr_id in every session or all of them for an
    unscoped change, and Federation::identity_changed is the entry point a PMIR
    subscription calls. The binding lifetime, federation.binding_ttl_ms, is the
    bound until one exists. Track 8 stays provisional and is not claimed.
  • The ask-all node selection of a deployment with no localizer (#46; §4.3
    Variant B, N4, N10). federation.node_selection = "ask-all" declares it,
    and a gateway that federates refuses to boot without the declaration
    (NodeSelectionUndeclared), so the choice is never a silent default. Every
    active member's cross-reference is asked, the query reaches only the members
    that return an ehr_id, and the others are not-resolved without failing
    the query (N6, N8). The selection is named in the startup log; the
    OPTIONS self-description follows with #73. The quickstart and the
    configuration page declare it.
  • A PIX Manager in the test harness, seeded by ITI-104 (#47). The testkit's
    pix::PixManager is a test device, not a PIXm implementation: an in-process
    loopback server that takes the PIXm 3.1.0 Patient Identity Feed FHIR
    (ITI-104, a conditional PUT Patient?identifier= held to the
    IHE.PIXm.Patient minimums, plus the Remove Patient Option's conditional
    DELETE) and answers ITI-83 $ihe-pix from what was fed, with each case of
    ITI TF-2 §3.83.4.2.2. The seed builder feeds it a synthetic patient in the
    urn:oid:2.999 arc with one ehr_id per node domain (seed::feed), and a
    capturing proxy in front of it journals and faults its traffic. The server's
    e2e suite now resolves through it, at one member and at both, while the
    failure classes stay on stubbed Managers. The IG's ITI-104 artefacts (the
    Source CapabilityStatement, the Patient profiles and the example
    Patients) are added to the vendored PIXm corpus.
  • The outbound identifier-hygiene gate and the security events of the
    analysis guard (#45, §5.4, N33, CP-26). Right before a request leaves for a
    node, the engine re-reads its AQL text (raw and as the printer escapes a
    literal), its paging members, its URL (raw and percent-decoded) and the
    headers the gateway adds, against the identifiers resolution consumed, and
    refuses to send a request that still carries one: nothing reaches the node,
    the query fails closed, and the refusal names the part of the request and
    never the value. The gate reads past the ehr_id literal the rewrite
    scoped the query to, so a short identifier that occurs inside the node's
    own ehr_id is answered. Every patient predicate the rewrite strips, every refused
    query and every gate stop is a security event under ferrofed::security,
    located by byte range and carrying no identifier. A clinician or facility
    predicate (composer, care facility, performer, committer) is dispatched
    unchanged; the same path compared with the patient identifier is refused.
    openehr-federation 0.0.5 adds Refusal::kind, Refusal::at and
    PatientQuery::stripped for those events.
  • The resolution step through a PIX Manager (#43). A [pixm] table selects
    the PIXm resolver: each [[pixm.manager]] names a PIX Manager's FHIR base,
    its credentials, and the ehr_id domain of every member it resolves, and
    [pixm.namespaces] maps a client's issuing namespace to a PIX assigning
    authority. One ITI-83 call per Manager resolves the patient at its members;
    only a member that knows the patient is asked its node query, a member that
    does not is not-resolved in meta.federation, a patient known nowhere is
    a 200 with no rows and complete: false, and a Manager that cannot answer
    fails the query 424 with no node asked. The patient identifier and its
    namespace reach the PIX Manager only. Boot is refused when a member has no
    Manager, when a member has two, or when [dev] and [pixm] are both set.
    The {node, ehr_id} set of each resolution is held in memory as the client
    session's resolution bindings, bounded by federation.binding_ttl_ms
    (default 15 minutes), and nothing derived from a patient identifier is
    stored or logged.
  • The PIXm ITI-83 client (#42): ihe-iti 0.0.3, feature pixm, asks a PIX
    Manager's Patient/$ihe-pix for the identifiers other domains hold for a
    patient, held to the vendored PIXm 3.1.0 OperationDefinition and read into
    a cross-reference, the profile's not-found answer, or a typed error (an
    unknown source or target domain, a rejection with its issue types, a
    timeout, a transport failure, a malformed answer). A 404 without a
    not-found issue never reads as "patient unknown". Identifier values are
    redacted in Debug and carried by no error. The IHE PIXm 3.1.0 package
    artefacts of ITI-83 are vendored under docs/specs/ihe-pixm/ by
    scripts/vendor/ihe-pixm.sh, pinned by version and tarball sha256, and the
    FHIR R4 model comes from fhir-types 0.1.107.
  • The fuzz lane (#134): four cargo fuzz targets over the untrusted inputs
    (the AQL rewrite over arbitrary text and parameters, the ITS-REST
    AdhocQueryExecute body through the façade's intake, a federated
    RESULT_SET with its meta.federation, and the OPTIONS {base}/ body),
    with seeds generated from the vendored golden cases and the specification's
    JSON examples by scripts/fuzz/seeds.sh. The rewrite target asserts the
    identifier-hygiene property of §5.4.1 (N33) on every query it accepts,
    string-function reconstructions included. fuzz.yml runs the targets
    weekly, on dispatch and on pull requests touching the code they read.
  • Both patient-identifier carriers resolve (#44; §5.4.3, N33, CP-38): an
    ENTRY-level subject predicate, …/subject/identifiers/id, is resolution
    input on equal terms with EHR_STATUS.subject.external_ref, with the
    DV_IDENTIFIER issuer or type as its issuing namespace. It is consumed
    and stripped exactly as external_ref is, so the same patient query through
    either carrier sends the same node query and returns the same rows. The same
    value in both carriers is consumed once; a second, different value, and
    qualifiers that name two namespaces, are refused with a 400, as are the
    assigner, a predicate on the identifier list, an ENTRY-level
    external_ref, and the carrier selected, ordered on or inside a function.
    openehr-federation is 0.0.4, and the interim Refusal::EntrySubject is
    gone.
  • The release lane at SLSA Build Level 3 (#31): the reusable
    release-build.yml builds ferrofed per target with cargo auditable,
    writes a CycloneDX and a syft SBOM, and attests the tarball's provenance and
    both SBOMs; the reusable release-image.yml builds the linux/amd64 and
    linux/arm64 image from the attested musl binaries, pushes it to
    ghcr.io/ferrohealth/ferrofed by digest, and attests the index and each
    platform manifest. A release is published only when the draft carries all
    eight assets of every target, and a pre-release is never marked latest.
  • The first federated query (#38): POST {base}/v1/query/aql answers one
    ITS-REST RESULT_SET over every member of the registry, with no federation
    syntax needed (N1, CP-1).
    • The gateway types the query_parameters, analyses the query with
      openehr-federation's aql rewrite, resolves the patient at every member
      through the configured cross-reference, and sends each member that knows
      the patient standard AQL keyed on its own ehr_id (N2, N7, CP-2, CP-4).
    • It fans out under the budget and re-injects the selected subject column
      as the resolution input (N5, CP-7). meta.federation names every endpoint
      (N16): a member that does not know the patient is not-resolved, a
      suspended endpoint and a second endpoint of one member are excluded.
    • A refused query is a 400 ITS-REST error that locates the fault and
      quotes nothing (§5.4.3). Without a cross-reference a patient query fails
      closed with 424 (decision A17), and without a registry the route stays
      501.
    • The configuration gains profile, [registry] document, [federation]
      (per_node_timeout_ms, overall_timeout_ms below the request timeout,
      default_namespace) and the [[dev.crossref]] rows of the development
      profile. config check loads the registry too.
    • The compose quickstart mounts docker/quickstart/registry.toml and
      ferrofed.toml, and the README shows one federated query returning the
      EHR of both nodes. The end-to-end test runs it against FerroEHR and
      EHRbase behind their capturing proxies, and no node request carries the
      patient identifier in any carrier (N33).
  • The fan-out engine, first increment (#37): ferrofed-engine's fanout
    module sends one request per in-scope node at once, each under a per-node
    deadline cut to the overall budget, with no retry and no hedging (§11.5,
    N38). A node still outstanding when the overall budget runs out is abandoned
    and reported time-out, without touching any other request, and its late
    answer contributes nothing. meta.federation is built from every outcome
    before the decision, so a failing answer carries it, with the effective
    timeout budget and complete: false. Under the all-or-nothing default an
    offline or time-out node fails the query 504, a node-error fails it
    424, 504 taking precedence; not-resolved and consent-denied fail
    nothing, so a patient found nowhere is a 200 with empty rows (§11.3, §11.4,
    N6, N37). Every endpoint record carries its node, system_id, managing
    organisation, base URL and latency_ms (§9.5, N40), and the rows of the
    active nodes are concatenated in endpoint id order until the merge (#52).
  • The crates.io lane behind the workspace publish switch (#32):
    publish-crates.yml runs on every v* tag, reads the publishable set from
    cargo metadata, packages it and publishes it in dependency order through
    crates.io Trusted Publishing, and is a successful no-op while the root
    [workspace.package] publish is false. The publish-dry-run job packages
    every library crate on every pull request, and
    scripts/release/publish-crates.sh is the shared implementation.
  • Node dispatch (#34): ferrofed-engine's dispatch module builds one
    openehr-its client per registry endpoint, rooted at the endpoint's base URL
    as the registry holds it plus the ITS-REST v1 segment (N28), and sends each
    node query as the generated POST {base}/v1/query/aql, once, with the
    per-node deadline and the gateway's request id. The answer maps to exactly
    one §11.1 status: a result set to active, a refused connection or broken
    stream to offline with its reason, a passed deadline to time-out, and a
    documented error, an undocumented status or a body that is not a result set
    to node-error carrying the node's own status and message (§11.2, N16,
    N40). A credential the provider cannot produce, or a request the client
    runtime refuses to compose, is a typed dispatch error, never an endpoint
    status. The engine names no HTTP engine directly, and a test holds it.
  • The AQL rewrite, first increment (#35): the aql feature of
    openehr-federation 0.0.3 parses a façade query with openehr-query, binds
    its query_parameters before analysis, and consumes the
    EHR_STATUS.subject.external_ref patient predicate with its namespace into a
    redacted Subject (§5.2). Each node receives the query scoped to its own
    ehr_id (§7.1, N7, N29); a selected subject or namespace column is
    re-injected after the merge (N5); and columns[] is rendered from the façade
    query alone (N17). The rewrite refuses with a typed 400, never quoting the
    identifier: a predicate outside the top-level AND chain or under another
    operator (§7.1), an integer identifier (decision A6), two different patients
    (A7), an unqualified identifier with no default namespace (A5), a query with
    no patient where a localizer decides the node set (A8), paging members that
    disagree with the query (A10), cross-node OFFSET (§11.6.2), an undirected
    aggregate (N14), and the identifier anywhere else in the query (§5.4.1),
    including rebuilt by CONCAT, CONCAT_WS or SUBSTRING over split
    literals, which the rewrite folds before the value test; a string function
    over a literal that cannot be folded is refused on an identifier-bearing path
    (decision A4). The reference implementation's 17 golden cases run as a
    corpus, each adjudicated. CI's test lane runs every feature.
  • The static registry (#36): ferrofed-registry 0.0.1 loads the federation's
    membership from a reviewed TOML bootstrap document ([[organisation]],
    [[node]] with its system_id and [[node.identifier]], [[endpoint]])
    with deny_unknown_fields throughout into an immutable RegistrySnapshot.
    A document with a dangling reference, a duplicate id, a system_id shared by
    two nodes (compared without ASCII case), an endpoint without exactly one
    managing organisation, a connection type other than openehr-rest-query, an
    unusable base URL or a node without an endpoint refuses to load (N19, N20,
    N21, §12b.2). NodeId, EndpointId and SystemId are distinct types with
    no conversion between them (N32, §12a.1).
  • The resolver seam and the development cross-reference (#36):
    ferrofed-identity 0.0.1 carries PatientRef, the patient identifier
    redacted in every rendering and never serialized (§5.4, N33), the Resolver
    trait (N3, §5.2), and StaticResolver, a fixed table from a synthetic
    identifier to each member's ehr_id that a configuration can enable only
    under profile = "development". It is FerroFED's own testing device, not an
    identity binding.
  • The container image: docker/Dockerfile puts the ferrofed musl binary on
    distroless static, digest-pinned, as the numeric non-root user 65532, with
    no shell and a read-only root; scripts/release/stage-dist.sh stages the
    binaries of a published release after checking their checksums. compose.yaml
    starts the gateway beside two member CDRs, FerroEHR 4.3.1 and EHRbase 2.36.0,
    every image pinned by digest in docs/VERSIONS.md and held there by the
    versions guard (#30).
  • The test harness in tools/ferrofed-testkit (#39): FerroEHR 4.3.1 and
    EHRbase 2.36.0 as two nodes on their own documented databases, pinned by
    digest and started only behind the FERROFED_E2E gate; a capturing and
    fault proxy in front of each node, whose journal records every request
    (method, path, query, headers, body) and which refuses, delays or answers
    with a chosen status per node; and a synthetic seed builder that writes
    EHRs, the template and compositions over ITS-REST alone, with patient
    identifiers only inside the urn:oid:2.999 example arc. CI runs the
    container suite in its own e2e (containers) job, and the versions guard
    holds the image pins to docs/VERSIONS.md.

Fixed

  • ihe-iti 0.0.4 names PMIR's transactions as ITI-93 and ITI-94 in its
    description, feature list and README. ITI-104, which it attributed to PMIR,
    is the PIXm Patient Identity Feed FHIR (#47).

Changed

  • The repository moved to the FerroHEALTH organization,
    https://github.com/FerroHEALTH/FerroFED, with the roadmap board as
    https://github.com/orgs/FerroHEALTH/projects/1; every link, the image name
    ghcr.io/ferrohealth/ferrofed and the crates' repository follow it (#123).
  • The crate layout names every crate that may be published after the
    specification it implements, one crate per specification with a feature per
    layer or profile (#106): openehr-federation 0.0.1 (the Federation Tier
    wire types, formerly ferrofed-wire, with the aql and merge features),
    ihe-iti 0.0.1 (features pixm, pdqm, mcsd, pmir, xcpd) and
    nl-generic-functions 0.0.1 (features nvi, mitz, lrza, nuts-auth).
    The names are held on crates.io by 0.0.0 placeholders. ferrofed-registry,
    ferrofed-identity and ferrofed-engine move under app/ and are never
    published. A new CI job lints every feature of the published crates on its
    own, and the architecture test also fails when a binding crate depends on
    FerroFED.
  • The openehr-* family moves to 0.0.74, the lockstep release with the AST
    visitor, spans, parameter binding and the federation directive in
    openehr-query, and the router builder, operation matcher, credentials
    provider and per-call options in openehr-its (FerroEHR #3505 to #3514).

Security

  • A configuration that does not parse no longer quotes its source line
    (#133). The TOML reader's error printed the offending line, so a malformed
    [dev] row could put its patient identifier, and a mistyped inline secret
    its value, into the config check output and the boot failure. The refusal
    now names the file, the line and column, the key path (dev alone for any
    line of the [dev] table) and the kind of fault, and none of the text.