Documentation
¶
Overview ¶
Package tefca implements the parts of TEFCA participation that do not require a QHIN connection.
What is real ¶
Purpose-of-use validation, configuration validation, and the audit trail - including its persistence, its query interface and its obligation to record failed attempts as well as successful ones. That last part is not a technicality: a run of failed deliveries to one recipient is how a misconfigured partner shows up, and a run of failed queries for different patients is how probing shows up.
What is not ¶
The network. There is no QHIN transport in this build, so no exchange happens, and every exchange function refuses with ErrExchangeNotImplemented rather than returning success.
That distinction was not always drawn, and the way it went wrong is worth keeping. All four exchange functions used to validate their inputs, make no network call, and return success; Deliver returned an Accepted response with a tracking identifier built from the clock. The audit layer then recorded each as a completed exchange - in the log that exists to prove what was exchanged when somebody asks months later. Four tests asserted that behaviour and passed.
What completing it requires ¶
Onboarding with a Qualified Health Information Network, mutually authenticated TLS using certificates issued through that process, and either the IHE profiles the Common Agreement started with or Facilitated FHIR as defined by the Sequoia Project's standard operating procedure, effective 8 March 2026. None of it can be written speculatively: it is tested against a real QHIN or it is not tested, and an untested exchange implementation is the same defect as no implementation wearing a better disguise.
Index ¶
- Constants
- Variables
- func Notify(ctx context.Context, cfg TEFCAConfig, req NotificationRequest) error
- func NotifyWithAudit(ctx context.Context, cfg TEFCAConfig, req NotificationRequest, ...) error
- func ValidPurpose(purpose string) bool
- type AuditLog
- type DeliveryRequest
- type DeliveryResponse
- type Exchange
- type ExchangeConfig
- type FHIRBundle
- type NotificationRequest
- type Participant
- func (p *Participant) AttachExchange(e *Exchange)
- func (p *Participant) Config() TEFCAConfig
- func (p *Participant) Deliver(ctx context.Context, req DeliveryRequest) (*DeliveryResponse, error)
- func (p *Participant) Exchange() *Exchange
- func (p *Participant) Notify(ctx context.Context, req NotificationRequest) error
- func (p *Participant) Query(ctx context.Context, req QueryRequest) (*QueryResponse, error)
- func (p *Participant) QueryFHIR(ctx context.Context, req QueryRequest) (*FHIRBundle, error)
- func (p *Participant) Retrieve(ctx context.Context, req RetrievalRequest) (*RetrievalResponse, error)
- type PersistentAuditLog
- func (a *PersistentAuditLog) Close() error
- func (a *PersistentAuditLog) LastError() error
- func (a *PersistentAuditLog) Path() string
- func (a *PersistentAuditLog) ReadAll(ctx context.Context, from, to time.Time) ([]TEFCAAudit, error)
- func (a *PersistentAuditLog) Record(entry TEFCAAudit)
- func (a *PersistentAuditLog) RecordErr(entry TEFCAAudit) error
- func (a *PersistentAuditLog) Skipped() int
- type QueryRequest
- type QueryResponse
- type RetrievalRequest
- type RetrievalResponse
- type TEFCAAudit
- type TEFCAConfig
Constants ¶
const ( PurposeTreatment = "treatment" PurposePayment = "payment" PurposeOperations = "operations" PurposePublicHealth = "public_health" PurposeIndividualAccess = "individual_access" )
Purpose of Use constants per TEFCA specification.
Variables ¶
var ErrExchangeNotImplemented = errors.New(
"tefca: this build has no QHIN transport, so nothing was exchanged. " +
"Purpose-of-use checking, configuration validation and the audit trail are real and work; the network call is not " +
"implemented and is refused rather than reported as a success. Onboarding with a QHIN, mutually authenticated TLS and " +
"either the IHE profiles or Facilitated FHIR are what remain")
ErrExchangeNotImplemented means no transport exists to carry a TEFCA exchange.
Why this error exists rather than a stub that returns success ¶
Every exchange function in this package validated its inputs, made no network call, and returned success. Deliver went further and returned Accepted with a fabricated tracking identifier. The audit layer above then recorded the exchange as having happened - in a persistent log whose entire purpose is to prove what was exchanged when somebody asks months later, prompted by a complaint or an investigation.
The audit code's own comment says that a trail disagreeing with what happened is "worse than no trail, because the trail is what gets believed". That was exactly right, and the layer underneath it made the comment false.
So the functions refuse. A refusal is auditable as a failure, which TEFCA requires anyway, and it cannot be mistaken for an exchange. This project has a standing invariant that a feature which cannot work yet is refused at load rather than silently skipped at run time; this is that rule applied one level down, because the audit trail is real and worth keeping available even while exchange is not.
What is missing ¶
A QHIN transport: mutually authenticated TLS to a Qualified Health Information Network, using certificates issued through QHIN onboarding, speaking either the IHE profiles the Common Agreement started with or Facilitated FHIR as defined by the Sequoia Project's standard operating procedure. None of that can be written speculatively - it is tested against a real QHIN or it is not tested at all.
Functions ¶
func Notify ¶
func Notify(ctx context.Context, cfg TEFCAConfig, req NotificationRequest) error
Notify broadcasts an event notification across the TEFCA network.
func NotifyWithAudit ¶
func NotifyWithAudit(ctx context.Context, cfg TEFCAConfig, req NotificationRequest, auditLog *AuditLog) error
NotifyWithAudit broadcasts a notification and records the exchange whether or not it succeeded.
A notification is the pattern most easily mistaken for not needing an audit entry, because it carries no patient record and asks for nothing back. But it discloses that a named patient had an event at a named organisation, and to everybody subscribed. That is a disclosure, and TEFCA does not exempt it.
func ValidPurpose ¶
ValidPurpose reports whether purpose is a recognized TEFCA Purpose of Use.
Types ¶
type AuditLog ¶
type AuditLog struct {
// contains filtered or unexported fields
}
AuditLog records and queries TEFCA audit entries. It is safe for concurrent use.
func (*AuditLog) Query ¶
func (a *AuditLog) Query(from, to time.Time) []TEFCAAudit
Query returns all audit entries whose timestamp falls within [from, to].
func (*AuditLog) Record ¶
func (a *AuditLog) Record(entry TEFCAAudit)
Record appends an audit entry to the log.
type DeliveryRequest ¶
DeliveryRequest describes a FHIR bundle push to another TEFCA participant.
type DeliveryResponse ¶
DeliveryResponse describes the outcome of a message delivery.
func Deliver ¶
func Deliver(ctx context.Context, cfg TEFCAConfig, req DeliveryRequest) (*DeliveryResponse, error)
Deliver pushes a FHIR bundle to the specified recipient via the QHIN.
func DeliverWithAudit ¶
func DeliverWithAudit(ctx context.Context, cfg TEFCAConfig, req DeliveryRequest, auditLog *AuditLog) (*DeliveryResponse, error)
DeliverWithAudit pushes a bundle and records the exchange whether or not it succeeded.
Failures are audited too, and that is not a technicality. A run of failed deliveries to one recipient is how a misconfigured partner shows up, and a run of failed queries for different patients is how probing shows up. Auditing only successes hides exactly the patterns an audit trail exists to reveal.
type Exchange ¶
type Exchange struct {
// contains filtered or unexported fields
}
Exchange is a configured Facilitated FHIR client.
Holds the registered client_id and the current access token, because re-registering on every query would be both rude and slow, and re-fetching a token that has fifty minutes left is a request nobody needs.
func NewExchange ¶
func NewExchange(participant TEFCAConfig, cfg ExchangeConfig, client *http.Client) (*Exchange, error)
NewExchange builds an exchange from configuration, reading the key, certificate and trust anchors from disk.
Refuses at construction rather than at first use. A participant who has misconfigured their certificate should find out when the server starts, not when the first clinician is waiting for a record - which is the standing rule in this repository that a feature which cannot work is refused at load.
func (*Exchange) Discover ¶
func (e *Exchange) Discover(ctx context.Context) (*udap.Metadata, *udap.SignedMetadataClaims, error)
Discover fetches and verifies the partner's UDAP metadata.
Separated from the query so the endpoints can be checked without exchanging anything, which is what the GUI's test button does and what somebody debugging an onboarding needs first.
func (*Exchange) QueryPatient ¶
func (e *Exchange) QueryPatient(ctx context.Context, req QueryRequest) (*FHIRBundle, error)
QueryPatient performs a Facilitated FHIR patient search against the partner and returns the bundle.
The search parameters are the ones the SOP's patient matching section rests on. Matching on a name alone is refused by the validation in Query below, and for a reason worth restating: a wrong match does not produce an error, it produces somebody else's medical record.
type ExchangeConfig ¶
type ExchangeConfig struct {
// PartnerFHIRBase is the FHIR base URL of the responding participant. UDAP metadata is discovered relative to it.
PartnerFHIRBase string `yaml:"partner_fhir_base"`
// ClientURI identifies this client to the trust community and must appear as a URI in the certificate's subject alternative names.
ClientURI string `yaml:"client_uri"`
// TrustAnchorPath holds the community's certificate authorities in PEM form.
//
// Required. Verifying a server's signed metadata against the certificate that arrived inside it proves only that one party made both,
// which was a real SAML defect in this repository: the verifier preferred the embedded certificate and authenticated an attacker as
// an administrator while every test passed.
TrustAnchorPath string `yaml:"trust_anchor_path"`
// ClientName and Contacts go into the software statement at registration. The specification requires a mailto address so that the
// other side has somebody to tell when an exchange starts failing.
ClientName string `yaml:"client_name"`
Contacts []string `yaml:"contacts"`
// Scope is the space-delimited list requested at registration. System scopes, since this is machine to machine.
Scope string `yaml:"scope"`
// ClientID, once a server has assigned one. Empty means register on first use.
ClientID string `yaml:"client_id,omitempty"`
// AllowUnanchoredMetadata relaxes chain verification of the partner's signed metadata.
//
// For sandboxes whose community authority nobody would install. Never for production: with it on, any party can sign a metadata
// document and be believed about where to send client assertions.
AllowUnanchoredMetadata bool `yaml:"allow_unanchored_metadata,omitempty"`
// PurposeCodes chooses the code system the purpose of use is sent in: empty or "tefca" for the TEFCA Exchange
// Purpose codes the Facilitated FHIR SOP requires, "hl7" for the HL7 v3 PurposeOfUse codes a UDAP community outside
// TEFCA (HL7 FAST SSRAA) expects. A partner refuses a token request in the system it does not use.
PurposeCodes string `yaml:"purpose_codes,omitempty"`
}
ExchangeConfig is what a Facilitated FHIR exchange needs beyond the participant details in TEFCAConfig.
func (ExchangeConfig) Validate ¶
func (e ExchangeConfig) Validate() error
Validate checks the exchange configuration.
type FHIRBundle ¶
type FHIRBundle struct {
Raw []byte // JSON-encoded FHIR Bundle
}
FHIRBundle is a placeholder for a FHIR Bundle resource returned from a query.
type NotificationRequest ¶
type NotificationRequest struct {
EventType string
PatientID string
Bundle []byte
// Purpose is why the disclosure is being made.
//
// Absent from the first version of this type, which made notification the one pattern that could disclose
// without stating why. A notification is easy to think of as exempt: it carries no record and asks for nothing
// back. But it tells everybody subscribed that a named patient had an event at a named organisation, and that
// is a disclosure. TEFCA does not exempt it, and an audit entry with an empty purpose column is an audit entry
// that answers no question anybody asks.
Purpose string
}
NotificationRequest describes an ADT-style event notification broadcast.
type Participant ¶
type Participant struct {
// contains filtered or unexported fields
}
Participant is a configured TEFCA participant that audits every exchange.
Constructed once and reused. The audit log travels with it, so no call site has to remember to pass one - which is the failure mode the wrappers leave open, because a wrapper that takes an audit log as a parameter can be called with a fresh empty one and nobody notices.
func NewParticipant ¶
func NewParticipant(cfg TEFCAConfig, audit *AuditLog) (*Participant, error)
NewParticipant returns a participant, refusing a configuration that cannot work.
Validated here rather than on each call. A participant that exists is a participant that can exchange, and discovering at the moment of a real query that the OID was never set is discovering it at the worst time.
func (*Participant) AttachExchange ¶ added in v0.1.23
func (p *Participant) AttachExchange(e *Exchange)
AttachExchange gives the participant its Facilitated FHIR exchange.
func (*Participant) Config ¶
func (p *Participant) Config() TEFCAConfig
Config returns the participant's configuration.
func (*Participant) Deliver ¶
func (p *Participant) Deliver(ctx context.Context, req DeliveryRequest) (*DeliveryResponse, error)
Deliver pushes a bundle to another participant, auditing the attempt.
func (*Participant) Exchange ¶ added in v0.1.23
func (p *Participant) Exchange() *Exchange
Exchange is the Facilitated FHIR exchange, nil when none is configured.
func (*Participant) Notify ¶
func (p *Participant) Notify(ctx context.Context, req NotificationRequest) error
Notify broadcasts an event notification, auditing the attempt.
func (*Participant) Query ¶
func (p *Participant) Query(ctx context.Context, req QueryRequest) (*QueryResponse, error)
Query performs an Individual Access Services query, auditing the attempt.
func (*Participant) QueryFHIR ¶ added in v0.1.23
func (p *Participant) QueryFHIR(ctx context.Context, req QueryRequest) (*FHIRBundle, error)
QueryFHIR searches the Facilitated FHIR partner for a patient, auditing the attempt whatever its outcome.
The purpose must be one this participant declares: a query under a purpose the organisation never agreed to is refused here rather than by the partner, so the refusal is ours to explain and is in our own trail.
func (*Participant) Retrieve ¶
func (p *Participant) Retrieve(ctx context.Context, req RetrievalRequest) (*RetrievalResponse, error)
Retrieve fetches a document, auditing the attempt.
type PersistentAuditLog ¶
type PersistentAuditLog struct {
AuditLog
// contains filtered or unexported fields
}
PersistentAuditLog records exchanges to a file as well as in memory.
The in-memory copy is kept because the interface reads recent entries constantly and re-reading a file that grows without bound for every page refresh would make the audit view slower the longer a site has been compliant.
func OpenAuditLog ¶
func OpenAuditLog(path string, retain time.Duration) (*PersistentAuditLog, error)
OpenAuditLog opens or creates an audit file.
Existing entries are read back, so a restart does not present an empty trail - which would read as "no exchanges have happened" rather than "this process started recently".
func (*PersistentAuditLog) Close ¶
func (a *PersistentAuditLog) Close() error
Close flushes and closes the file.
func (*PersistentAuditLog) LastError ¶
func (a *PersistentAuditLog) LastError() error
LastError returns the most recent failure to write the audit file, if any.
Surfaced deliberately. A site whose audit file has become unwritable is out of compliance and needs to know today, not when somebody asks for records.
func (*PersistentAuditLog) Path ¶
func (a *PersistentAuditLog) Path() string
Path returns where the trail is being written, so the interface can say.
func (*PersistentAuditLog) ReadAll ¶
func (a *PersistentAuditLog) ReadAll(ctx context.Context, from, to time.Time) ([]TEFCAAudit, error)
ReadAll returns every entry in the file within a window, including ones outside the memory window.
Reads the file rather than memory, because the questions auditors ask are about last year and memory holds weeks. The context is honoured so a request for a very large range can be abandoned.
func (*PersistentAuditLog) Record ¶
func (a *PersistentAuditLog) Record(entry TEFCAAudit)
Record writes an entry to the file and keeps it in memory.
The file is written first. If the write fails the entry is still kept in memory and the error is returned, because losing the record entirely is the worse outcome and a caller that can report the failure is better than silence - but the caller has to be told, or a full disk becomes a silent gap in a compliance record.
func (*PersistentAuditLog) RecordErr ¶
func (a *PersistentAuditLog) RecordErr(entry TEFCAAudit) error
RecordErr is the same as Record but returns any failure to persist.
Offered because a caller that can tell somebody the audit trail is not being written should be given the chance.
func (*PersistentAuditLog) Skipped ¶
func (a *PersistentAuditLog) Skipped() int
Skipped returns how many lines could not be read when the log was loaded.
Reported rather than hidden. A non-zero count means part of the trail is unreadable, and that is something a site has to know about its own compliance record.
type QueryRequest ¶
type QueryRequest struct {
PatientID string
PatientName string
DOB string
Gender string
Purpose string
}
QueryRequest describes a patient-matching query sent to the TEFCA network.
type QueryResponse ¶
type QueryResponse struct {
Records []FHIRBundle
SourceOrg string
Timestamp time.Time
}
QueryResponse contains the results of a TEFCA query.
func Query ¶
func Query(ctx context.Context, cfg TEFCAConfig, req QueryRequest) (*QueryResponse, error)
Query performs a TEFCA Individual Access Services query against the configured QHIN endpoint. The context controls cancellation and deadlines.
func QueryWithAudit ¶
func QueryWithAudit(ctx context.Context, cfg TEFCAConfig, req QueryRequest, auditLog *AuditLog) (*QueryResponse, error)
QueryWithAudit performs a TEFCA query and records an audit entry for the exchange regardless of success or failure. Every exchange MUST be audited per TEFCA requirements — including failed attempts, which could indicate probing attacks.
type RetrievalRequest ¶
RetrievalRequest describes a request to fetch a specific document by reference.
type RetrievalResponse ¶
RetrievalResponse contains the retrieved document.
func Retrieve ¶
func Retrieve(ctx context.Context, cfg TEFCAConfig, req RetrievalRequest) (*RetrievalResponse, error)
Retrieve fetches a specific document from the TEFCA network by reference.
func RetrieveWithAudit ¶
func RetrieveWithAudit(ctx context.Context, cfg TEFCAConfig, req RetrievalRequest, auditLog *AuditLog) (*RetrievalResponse, error)
RetrieveWithAudit fetches a document and records the exchange whether or not it succeeded.
type TEFCAAudit ¶
type TEFCAAudit struct {
Timestamp time.Time
Direction string // "inbound" or "outbound"
Purpose string
PatientID string
RequestingOrg string
RespondingOrg string
ExchangeType string // "query", "delivery", "retrieval", "notification"
Success bool
ErrorDetail string
}
TEFCAAudit represents a single TEFCA-mandated audit entry.
type TEFCAConfig ¶
type TEFCAConfig struct {
// OrganizationName is the human-readable name of the participating organization.
OrganizationName string
// OrganizationOID is the OID assigned to the organization.
OrganizationOID string
// QHINEndpoint is the base URL of the Qualified Health Information Network.
QHINEndpoint string
// ParticipantType is one of: provider, payer, public_health.
ParticipantType string
// CertificatePath is the filesystem path to the mTLS certificate.
CertificatePath string
// KeyPath is the filesystem path to the mTLS private key.
KeyPath string
// SupportedPurposes lists the purposes of use this participant supports.
SupportedPurposes []string
}
TEFCAConfig holds the configuration for a TEFCA network participant.
func (TEFCAConfig) Validate ¶
func (c TEFCAConfig) Validate() error
Validate checks that all required TEFCAConfig fields are present and valid.